pan-wizard 3.34.0 → 3.35.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 +6 -6
- package/agents/pan-conductor.md +2 -2
- package/agents/pan-debugger.md +3 -1
- package/agents/pan-design-checker.md +1 -0
- package/agents/pan-executor.md +18 -1
- package/agents/pan-experiment-runner.md +1 -1
- package/agents/pan-hardener.md +1 -1
- package/agents/pan-meta-reviewer.md +1 -1
- package/agents/pan-phase-researcher.md +2 -2
- package/agents/pan-plan-checker.md +9 -8
- package/agents/pan-planner.md +3 -1
- package/agents/pan-project-researcher.md +1 -1
- package/agents/pan-research-synthesizer.md +1 -1
- package/agents/pan-reviewer.md +3 -2
- package/agents/pan-verifier.md +1 -1
- package/bin/install-lib.cjs +119 -16
- package/bin/install.js +34 -62
- package/commands/pan/cost.md +0 -4
- package/commands/pan/dashboard.md +0 -3
- package/commands/pan/design-phase.md +2 -2
- package/commands/pan/hud.md +1 -5
- package/commands/pan/knowledge.md +0 -1
- package/commands/pan/mcp-bridge.md +1 -3
- package/commands/pan/optimize.md +2 -0
- package/commands/pan/phase-tests.md +1 -1
- package/commands/pan/preview.md +1 -2
- package/commands/pan/report.md +1 -5
- package/commands/pan/research-phase.md +4 -4
- package/commands/pan/review-deep.md +0 -2
- package/commands/pan/what-if.md +1 -2
- package/hooks/dist/pan-context-monitor.js +12 -3
- package/hooks/dist/pan-guard.js +244 -0
- package/hooks/dist/pan-statusline.js +5 -2
- package/package.json +1 -1
- package/pan-wizard-core/bin/lib/commands.cjs +24 -2
- package/pan-wizard-core/bin/lib/config.cjs +5 -0
- package/pan-wizard-core/bin/lib/findings.cjs +126 -19
- package/pan-wizard-core/bin/lib/hygiene.cjs +11 -5
- package/pan-wizard-core/bin/lib/init.cjs +6 -0
- package/pan-wizard-core/bin/lib/optimize.cjs +13 -1
- package/pan-wizard-core/bin/lib/phase-gate.cjs +174 -0
- package/pan-wizard-core/bin/lib/phase.cjs +24 -0
- package/pan-wizard-core/bin/lib/preview.cjs +32 -31
- package/pan-wizard-core/bin/lib/verdict.cjs +140 -10
- package/pan-wizard-core/bin/lib/verify-integrity.cjs +181 -0
- package/pan-wizard-core/bin/lib/verify-plan-set.cjs +230 -0
- package/pan-wizard-core/bin/lib/verify-scope.cjs +6 -5
- package/pan-wizard-core/bin/lib/whatif.cjs +48 -6
- package/pan-wizard-core/bin/pan-tools.cjs +13 -4
- package/pan-wizard-core/mcp/merge-gate.cjs +14 -3
- package/pan-wizard-core/mcp/native-tools.cjs +3 -3
- package/pan-wizard-core/opencode/pan-wizard.js +36 -8
- package/pan-wizard-core/references/model-profiles.md +1 -1
- package/pan-wizard-core/references/planning-config.md +3 -3
- package/pan-wizard-core/references/verdict-contract.md +6 -1
- package/pan-wizard-core/templates/config.json +2 -6
- package/pan-wizard-core/templates/debug-subagent-prompt.md +2 -1
- package/pan-wizard-core/templates/phase-prompt.md +2 -0
- package/pan-wizard-core/templates/planner-subagent-prompt.md +2 -0
- package/pan-wizard-core/templates/preview-report.md +1 -1
- package/pan-wizard-core/templates/state.md +4 -4
- package/pan-wizard-core/workflows/diagnose-issues.md +4 -4
- package/pan-wizard-core/workflows/discuss-phase.md +2 -1
- package/pan-wizard-core/workflows/exec-phase.md +51 -40
- package/pan-wizard-core/workflows/milestone-gaps.md +2 -2
- package/pan-wizard-core/workflows/new-project.md +8 -16
- package/pan-wizard-core/workflows/plan-phase.md +22 -8
- package/pan-wizard-core/workflows/progress.md +1 -1
- package/pan-wizard-core/workflows/quick.md +2 -2
- package/pan-wizard-core/workflows/todo-check.md +3 -2
- package/pan-wizard-core/workflows/transition.md +4 -0
- package/pan-wizard-core/workflows/verify-phase.md +6 -6
- package/scripts/build-hooks.js +1 -0
- package/scripts/generate-skills-docs.py +2 -2
- package/scripts/prompt-lint.cjs +167 -0
- package/scripts/test-surface.cjs +1 -0
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
/**
|
|
3
|
+
* Plan checks in code (program V7).
|
|
4
|
+
*
|
|
5
|
+
* The plan-checker agent reads a phase's plans and judges them, but the parts of that
|
|
6
|
+
* judgement that are arithmetic were left to the model: whether every requirement is
|
|
7
|
+
* covered, whether two plans in one wave edit the same file, whether a dependency
|
|
8
|
+
* points at a plan that does not exist, at the same wave or a later one, or round in
|
|
9
|
+
* a circle. `verify plan-set` checks those in code, before the checker is spent.
|
|
10
|
+
*
|
|
11
|
+
* It also binds a passing plan check to the plans it checked. The recorder stores a
|
|
12
|
+
* hash of the phase's plan files with every plan-checker verdict (`plans_sha`), and
|
|
13
|
+
* `planCheckStatus` tells exec-phase whether the plans it is about to run are the
|
|
14
|
+
* plans that passed: `ok`, `stale` (edited since), `failed`, `missing`, `legacy`
|
|
15
|
+
* (a record from before hashing: a warning, never a refusal) or `disabled`.
|
|
16
|
+
*
|
|
17
|
+
* And it reports drift: a plan that records `planned_commit` lists the files it
|
|
18
|
+
* modifies that changed on the branch since it was planned.
|
|
19
|
+
*/
|
|
20
|
+
const fs = require('fs');
|
|
21
|
+
const path = require('path');
|
|
22
|
+
const crypto = require('crypto');
|
|
23
|
+
const { findPhaseInternal, output, error, execGit, loadConfig, toLf, toPosix } = require('./core.cjs');
|
|
24
|
+
const { extractFrontmatter } = require('./frontmatter.cjs');
|
|
25
|
+
const { roadmapRequirementIds } = require('./roadmap.cjs');
|
|
26
|
+
const { buildPlanIndex, dependencyNames } = require('./phase.cjs');
|
|
27
|
+
const { ROADMAP_FILE, getPlanId } = require('./constants.cjs');
|
|
28
|
+
const { planningPath, filterPlanFiles, filterSummaryFiles } = require('./utils.cjs');
|
|
29
|
+
|
|
30
|
+
const PLAN_CHECKER = 'pan-plan-checker';
|
|
31
|
+
|
|
32
|
+
const asList = (v) => (Array.isArray(v) ? v : (v == null || v === '' ? [] : [v])).map((x) => String(x).trim()).filter(Boolean);
|
|
33
|
+
const normFile = (f) => toPosix(String(f).trim()).replace(/^\.\//, '');
|
|
34
|
+
|
|
35
|
+
/** The phase directory and its plan and summary files, or `{error, reason}`. */
|
|
36
|
+
function phaseFiles(cwd, phase) {
|
|
37
|
+
const info = findPhaseInternal(cwd, phase);
|
|
38
|
+
if (!info || !info.found) return { error: 'phase_not_found', reason: `no phase directory for phase ${phase}` };
|
|
39
|
+
const dir = path.join(cwd, info.directory);
|
|
40
|
+
let files = [];
|
|
41
|
+
try { files = fs.readdirSync(dir); } catch (e) { return { error: 'unreadable_phase', reason: `cannot read ${info.directory}: ${e.code || e.message}` }; }
|
|
42
|
+
return { info, dir, planFiles: filterPlanFiles(files).sort(), summaryFiles: filterSummaryFiles(files) };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* A hash of a phase's plan files (names and line-ending-normalised contents), so a
|
|
47
|
+
* plan check can be matched to exactly the plans it checked.
|
|
48
|
+
*/
|
|
49
|
+
function plansSha(dir, planFiles) {
|
|
50
|
+
const h = crypto.createHash('sha256');
|
|
51
|
+
for (const f of [...planFiles].sort()) {
|
|
52
|
+
let text = '';
|
|
53
|
+
try { text = fs.readFileSync(path.join(dir, f), 'utf-8'); } catch { /* unreadable reads as empty */ }
|
|
54
|
+
h.update(f + '\u0000' + toLf(text) + '\u0000');
|
|
55
|
+
}
|
|
56
|
+
return h.digest('hex').slice(0, 16);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** `plansSha` for a phase by number, or null when it has no plans. */
|
|
60
|
+
function plansShaFor(cwd, phase) {
|
|
61
|
+
const pf = phaseFiles(cwd, phase);
|
|
62
|
+
if (pf.error || !pf.planFiles.length) return null;
|
|
63
|
+
return plansSha(pf.dir, pf.planFiles);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** The phase's plans, each with its requirements, planned_commit and file name. */
|
|
67
|
+
function loadPlans(pf) {
|
|
68
|
+
const { plans } = buildPlanIndex(pf.dir, pf.planFiles, pf.summaryFiles);
|
|
69
|
+
const byId = new Map(plans.map((p) => [p.id, p]));
|
|
70
|
+
for (const file of pf.planFiles) {
|
|
71
|
+
const p = byId.get(getPlanId(file));
|
|
72
|
+
if (!p) continue;
|
|
73
|
+
let fm = {};
|
|
74
|
+
try { fm = extractFrontmatter(fs.readFileSync(path.join(pf.dir, file), 'utf-8')) || {}; } catch { /* unreadable */ }
|
|
75
|
+
p.file = file;
|
|
76
|
+
p.requirements = asList(fm.requirements);
|
|
77
|
+
p.planned_commit = fm.planned_commit ? String(fm.planned_commit).trim() : null;
|
|
78
|
+
p.files_modified = p.files_modified.map(normFile).filter(Boolean);
|
|
79
|
+
}
|
|
80
|
+
return plans;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** A dependency entry that names a plan of another phase (`02-01` from phase 03). */
|
|
84
|
+
function isOtherPhase(dep, phaseNumber) {
|
|
85
|
+
const m = /^(\d+(?:\.\d+)*)-\d+$/.exec(String(dep).trim());
|
|
86
|
+
if (!m) return false;
|
|
87
|
+
const strip = (s) => String(s).split('.').map((x) => String(parseInt(x, 10))).join('.');
|
|
88
|
+
return strip(m[1]) !== strip(phaseNumber);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Check a phase's plan set.
|
|
93
|
+
* @returns {{phase, ok, errors: Array, warnings: Array, plans: number}|{error, reason}}
|
|
94
|
+
*/
|
|
95
|
+
function checkPlanSet(cwd, phase) {
|
|
96
|
+
const pf = phaseFiles(cwd, phase);
|
|
97
|
+
if (pf.error) return pf;
|
|
98
|
+
if (!pf.planFiles.length) return { error: 'no_plans', reason: `phase ${phase} has no plans` };
|
|
99
|
+
const phaseNumber = pf.info.phase_number || String(phase);
|
|
100
|
+
const plans = loadPlans(pf);
|
|
101
|
+
const errors = [];
|
|
102
|
+
const warnings = [];
|
|
103
|
+
|
|
104
|
+
// 1. Requirement coverage, against the roadmap's requirement ids for this phase.
|
|
105
|
+
let roadmap = '';
|
|
106
|
+
try { roadmap = fs.readFileSync(planningPath(cwd, ROADMAP_FILE), 'utf-8'); } catch { /* no roadmap: nothing to cover */ }
|
|
107
|
+
const required = roadmapRequirementIds(roadmap, phaseNumber);
|
|
108
|
+
const key = (id) => String(id).trim().toUpperCase();
|
|
109
|
+
const planned = new Set(plans.flatMap((p) => p.requirements.map(key)));
|
|
110
|
+
for (const id of required) {
|
|
111
|
+
if (!planned.has(key(id))) errors.push({ code: 'uncovered_requirement', requirement: id, detail: `requirement ${id} is in the roadmap for phase ${phaseNumber} but no plan lists it` });
|
|
112
|
+
}
|
|
113
|
+
if (required.length) {
|
|
114
|
+
const known = new Set(required.map(key));
|
|
115
|
+
for (const p of plans) for (const r of p.requirements) {
|
|
116
|
+
if (!known.has(key(r))) warnings.push({ code: 'unknown_requirement', plan: p.id, requirement: r, detail: `plan ${p.id} lists ${r}, which the roadmap does not give phase ${phaseNumber}` });
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// 2. Dependencies: each must name a plan of this phase in an earlier wave.
|
|
121
|
+
const resolved = new Map(); // plan id → ids of the plans it depends on
|
|
122
|
+
for (const p of plans) {
|
|
123
|
+
const deps = [];
|
|
124
|
+
for (const d of p.depends_on) {
|
|
125
|
+
if (isOtherPhase(d, phaseNumber)) continue; // a finished phase's plan: not this set's to check
|
|
126
|
+
const target = plans.find((q) => q.id !== p.id && dependencyNames(d, q.id));
|
|
127
|
+
if (!target) {
|
|
128
|
+
if (dependencyNames(d, p.id)) errors.push({ code: 'self_reference', plan: p.id, detail: `plan ${p.id} depends on itself` });
|
|
129
|
+
else errors.push({ code: 'missing_reference', plan: p.id, depends_on: d, detail: `plan ${p.id} depends on "${d}", which names no plan in this phase` });
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
deps.push(target.id);
|
|
133
|
+
if (target.wave >= p.wave) {
|
|
134
|
+
errors.push({ code: 'future_reference', plan: p.id, depends_on: target.id, detail: `plan ${p.id} (wave ${p.wave}) depends on ${target.id} (wave ${target.wave}); a dependency must be in an earlier wave` });
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
resolved.set(p.id, deps);
|
|
138
|
+
const depWaves = deps.map((id) => plans.find((q) => q.id === id).wave);
|
|
139
|
+
const expected = depWaves.length ? Math.max(...depWaves) + 1 : 1;
|
|
140
|
+
if (p.wave > expected) {
|
|
141
|
+
warnings.push({ code: 'wave_gap', plan: p.id, detail: `plan ${p.id} is in wave ${p.wave} but its dependencies allow wave ${expected}` });
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// 3. Cycles.
|
|
146
|
+
const state = new Map();
|
|
147
|
+
const reported = new Set();
|
|
148
|
+
const visit = (id, trail) => {
|
|
149
|
+
if (state.get(id) === 'done') return;
|
|
150
|
+
if (state.get(id) === 'active') {
|
|
151
|
+
const cycle = [...trail.slice(trail.indexOf(id)), id];
|
|
152
|
+
const sig = [...new Set(cycle)].sort().join(',');
|
|
153
|
+
if (!reported.has(sig)) { reported.add(sig); errors.push({ code: 'cycle', plans: cycle, detail: `dependency cycle: ${cycle.join(' → ')}` }); }
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
state.set(id, 'active');
|
|
157
|
+
for (const d of resolved.get(id) || []) visit(d, [...trail, id]);
|
|
158
|
+
state.set(id, 'done');
|
|
159
|
+
};
|
|
160
|
+
for (const p of plans) visit(p.id, []);
|
|
161
|
+
|
|
162
|
+
// 4. Same-wave file overlap: two plans that run in parallel must not edit one file.
|
|
163
|
+
const dependsOn = (a, b, seen = new Set()) => {
|
|
164
|
+
if (seen.has(a)) return false;
|
|
165
|
+
seen.add(a);
|
|
166
|
+
return (resolved.get(a) || []).some((d) => d === b || dependsOn(d, b, seen));
|
|
167
|
+
};
|
|
168
|
+
for (let i = 0; i < plans.length; i++) {
|
|
169
|
+
for (let j = i + 1; j < plans.length; j++) {
|
|
170
|
+
const a = plans[i];
|
|
171
|
+
const b = plans[j];
|
|
172
|
+
if (a.wave !== b.wave) continue;
|
|
173
|
+
const shared = a.files_modified.filter((f) => b.files_modified.includes(f));
|
|
174
|
+
if (!shared.length || dependsOn(a.id, b.id) || dependsOn(b.id, a.id)) continue;
|
|
175
|
+
errors.push({ code: 'same_wave_overlap', plans: [a.id, b.id], files: shared, detail: `plans ${a.id} and ${b.id} both modify ${shared.join(', ')} in wave ${a.wave} with no dependency between them` });
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return { phase: phaseNumber, ok: errors.length === 0, errors, warnings, plans: plans.length, plans_sha: plansSha(pf.dir, pf.planFiles) };
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Whether the plans exec-phase is about to run are the plans that passed a plan check.
|
|
184
|
+
* @returns {{status: 'ok'|'stale'|'failed'|'missing'|'legacy'|'disabled', reason?: string, plans_sha?: string, checked_sha?: string, attempt?: number}}
|
|
185
|
+
*/
|
|
186
|
+
function planCheckStatus(cwd, phase) {
|
|
187
|
+
const config = loadConfig(cwd);
|
|
188
|
+
if (config.plan_checker === false) return { status: 'disabled', reason: 'workflow.plan_check is off in config.json' };
|
|
189
|
+
const F = require('./findings.cjs');
|
|
190
|
+
const V = require('./verdict.cjs');
|
|
191
|
+
const { verdicts } = F.foldFindings(F.readLedger(cwd).rows);
|
|
192
|
+
const mine = verdicts.filter((v) => v.agent === PLAN_CHECKER && V.comparablePhase(v.phase) === V.comparablePhase(phase));
|
|
193
|
+
const last = mine[mine.length - 1];
|
|
194
|
+
const current = plansShaFor(cwd, phase);
|
|
195
|
+
if (!last) return { status: 'missing', reason: 'no plan check is recorded for this phase', plans_sha: current };
|
|
196
|
+
if (last.outcome !== 'pass') return { status: 'failed', reason: `the last plan check (attempt ${last.attempt}) found issues`, attempt: last.attempt, plans_sha: current };
|
|
197
|
+
if (!last.plans_sha) return { status: 'legacy', reason: 'the plan check was recorded before plans were hashed, so it cannot show the plans are unchanged', attempt: last.attempt, plans_sha: current };
|
|
198
|
+
if (last.plans_sha !== current) return { status: 'stale', reason: 'the plans changed after they passed the plan check', attempt: last.attempt, checked_sha: last.plans_sha, plans_sha: current };
|
|
199
|
+
return { status: 'ok', attempt: last.attempt, plans_sha: current };
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Plans not yet executed whose modified files changed since they were planned.
|
|
204
|
+
* @returns {Array<{plan, planned_commit, files?: string[], error?: string}>}
|
|
205
|
+
*/
|
|
206
|
+
function planDrift(cwd, phase) {
|
|
207
|
+
const pf = phaseFiles(cwd, phase);
|
|
208
|
+
if (pf.error || !pf.planFiles.length) return [];
|
|
209
|
+
const out = [];
|
|
210
|
+
for (const p of loadPlans(pf)) {
|
|
211
|
+
if (p.has_summary || !p.planned_commit || !p.files_modified.length) continue;
|
|
212
|
+
if (!/^[0-9a-f]{4,40}$/i.test(p.planned_commit)) { out.push({ plan: p.id, planned_commit: p.planned_commit, error: 'not a commit sha' }); continue; }
|
|
213
|
+
const r = execGit(cwd, ['diff', '--name-only', p.planned_commit, 'HEAD', '--']);
|
|
214
|
+
if (r.exitCode !== 0) { out.push({ plan: p.id, planned_commit: p.planned_commit, error: 'unknown commit' }); continue; }
|
|
215
|
+
const changed = new Set(r.stdout.split(/\r?\n/).map(normFile).filter(Boolean));
|
|
216
|
+
const files = p.files_modified.filter((f) => changed.has(f));
|
|
217
|
+
if (files.length) out.push({ plan: p.id, planned_commit: p.planned_commit, files });
|
|
218
|
+
}
|
|
219
|
+
return out;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function cmdVerifyPlanSet(cwd, phase, raw) {
|
|
223
|
+
if (!phase) error('Usage: verify plan-set <phase>');
|
|
224
|
+
const r = checkPlanSet(cwd, phase);
|
|
225
|
+
if (r.error) { output(r, raw); return; }
|
|
226
|
+
const result = { ...r, plan_check: planCheckStatus(cwd, phase), drift: planDrift(cwd, phase) };
|
|
227
|
+
output(result, raw, r.ok ? 'valid' : 'invalid', r.ok ? 0 : 1);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
module.exports = { checkPlanSet, plansSha, plansShaFor, planCheckStatus, planDrift, cmdVerifyPlanSet, PLAN_CHECKER };
|
|
@@ -68,27 +68,28 @@ function exclusionReason(file, planningPrefix) {
|
|
|
68
68
|
|
|
69
69
|
/**
|
|
70
70
|
* Files the phase's plan commits touched, paths relative to `cwd` (git's --relative).
|
|
71
|
-
* @returns {{git: boolean, commits: number, files: Set<string
|
|
71
|
+
* @returns {{git: boolean, commits: number, files: Set<string>, hashes: string[]}} hashes newest first
|
|
72
72
|
*/
|
|
73
73
|
function committedFiles(cwd, phaseNumber) {
|
|
74
74
|
const r = execGit(cwd, ['-c', 'core.quotepath=off', 'log', '--no-merges', '-n', String(COMMIT_SCAN_LIMIT), '--relative', '--name-only', '--format=@@%H%x09%s']);
|
|
75
|
-
if (r.exitCode !== 0) return { git: false, commits: 0, files: new Set() };
|
|
75
|
+
if (r.exitCode !== 0) return { git: false, commits: 0, files: new Set(), hashes: [] };
|
|
76
76
|
// `{type}({phase}-{plan}):` with the phase's zero padding optional ("feat(03-01):", "fix(3-02)!:").
|
|
77
77
|
const unpadded = String(phaseNumber).replace(/^0+(?=\d)/, '');
|
|
78
78
|
const subjectRe = new RegExp(`^[a-z]+\\(0*${escapeRegex(unpadded)}-[0-9]+\\)!?:`);
|
|
79
79
|
const files = new Set();
|
|
80
|
+
const hashes = [];
|
|
80
81
|
let commits = 0;
|
|
81
82
|
let inPhase = false;
|
|
82
83
|
for (const line of r.stdout.split('\n')) {
|
|
83
84
|
if (line.startsWith('@@')) {
|
|
84
85
|
const subject = line.slice(2).split('\t').slice(1).join('\t');
|
|
85
86
|
inPhase = subjectRe.test(subject);
|
|
86
|
-
if (inPhase) commits++;
|
|
87
|
+
if (inPhase) { commits++; hashes.push(line.slice(2).split('\t')[0]); }
|
|
87
88
|
continue;
|
|
88
89
|
}
|
|
89
90
|
if (inPhase && line.trim()) files.add(normalisePath(line));
|
|
90
91
|
}
|
|
91
|
-
return { git: true, commits, files };
|
|
92
|
+
return { git: true, commits, files, hashes };
|
|
92
93
|
}
|
|
93
94
|
|
|
94
95
|
/**
|
|
@@ -223,4 +224,4 @@ function cmdVerifyStale(cwd, phase, raw) {
|
|
|
223
224
|
output(r, raw, r.error ? undefined : r.state);
|
|
224
225
|
}
|
|
225
226
|
|
|
226
|
-
module.exports = { scopePhase, cmdVerifyScope, verificationFreshness, cmdVerifyStale, coveredFiles, LOCKFILES };
|
|
227
|
+
module.exports = { scopePhase, cmdVerifyScope, verificationFreshness, cmdVerifyStale, coveredFiles, committedFiles, LOCKFILES, TEST_PATH_RE, TEST_FILE_RE };
|
|
@@ -210,10 +210,38 @@ function createWorktree(cwd, phaseNum, scenario, opts) {
|
|
|
210
210
|
};
|
|
211
211
|
}
|
|
212
212
|
|
|
213
|
+
/** Registered worktrees as {path, branch}; branch is null on a detached HEAD. */
|
|
214
|
+
function listWorktrees(cwd) {
|
|
215
|
+
const r = execGit(cwd, ['worktree', 'list', '--porcelain']);
|
|
216
|
+
if (r.exitCode !== 0) return [];
|
|
217
|
+
const list = [];
|
|
218
|
+
for (const line of r.stdout.split(/\r?\n/)) {
|
|
219
|
+
if (line.startsWith('worktree ')) list.push({ path: line.slice('worktree '.length), branch: null });
|
|
220
|
+
else if (line.startsWith('branch ') && list.length) list[list.length - 1].branch = line.slice('branch '.length).replace(/^refs\/heads\//, '');
|
|
221
|
+
}
|
|
222
|
+
return list;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function samePath(a, b) {
|
|
226
|
+
const norm = (p) => {
|
|
227
|
+
let r = path.resolve(p);
|
|
228
|
+
try { r = fs.realpathSync.native(r); } catch { /* gone: compare the resolved form */ }
|
|
229
|
+
r = toPosix(r);
|
|
230
|
+
return process.platform === 'win32' ? r.toLowerCase() : r;
|
|
231
|
+
};
|
|
232
|
+
return norm(a) === norm(b);
|
|
233
|
+
}
|
|
234
|
+
|
|
213
235
|
/**
|
|
214
236
|
* Remove a worktree + its branch. Best-effort: errors are surfaced but
|
|
215
237
|
* don't block subsequent cleanups.
|
|
216
238
|
*
|
|
239
|
+
* Only what-if's own objects are touched: the branch must carry BRANCH_PREFIX and the
|
|
240
|
+
* worktree, when git still lists it, must be checked out on that branch (or, with no
|
|
241
|
+
* branch given, on some BRANCH_PREFIX branch). Anything else is refused before a single
|
|
242
|
+
* git write, because `branch -D` and `worktree remove --force` destroy unmerged commits
|
|
243
|
+
* and uncommitted files.
|
|
244
|
+
*
|
|
217
245
|
* @param {string} cwd - Main project root (for the cleanup git call)
|
|
218
246
|
* @param {string} worktreePath - Path returned by createWorktree
|
|
219
247
|
* @param {string} branch - Branch name returned by createWorktree
|
|
@@ -224,15 +252,29 @@ function cleanupWorktree(cwd, worktreePath, branch, opts) {
|
|
|
224
252
|
if (!isGitRepo(cwd)) {
|
|
225
253
|
return { error: 'Not a git repo' };
|
|
226
254
|
}
|
|
255
|
+
if (branch && !branch.startsWith(BRANCH_PREFIX)) {
|
|
256
|
+
return { error: `Refusing to delete branch '${branch}': what-if only cleans up branches it created (${BRANCH_PREFIX}*)` };
|
|
257
|
+
}
|
|
258
|
+
const entry = listWorktrees(cwd).find(w => samePath(w.path, worktreePath));
|
|
259
|
+
if (entry) {
|
|
260
|
+
const owned = entry.branch && entry.branch.startsWith(BRANCH_PREFIX) && (!branch || entry.branch === branch);
|
|
261
|
+
if (!owned) {
|
|
262
|
+
return { error: `Refusing to remove worktree ${toPosix(worktreePath)}: it is on '${entry.branch || 'a detached HEAD'}', not ${branch || BRANCH_PREFIX + '*'}` };
|
|
263
|
+
}
|
|
264
|
+
}
|
|
227
265
|
const warnings = [];
|
|
228
266
|
const force = opts?.force === true;
|
|
229
267
|
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
268
|
+
if (entry) {
|
|
269
|
+
const rmArgs = ['worktree', 'remove'];
|
|
270
|
+
if (force) rmArgs.push('--force');
|
|
271
|
+
rmArgs.push(worktreePath);
|
|
272
|
+
const removeResult = execGit(cwd, rmArgs);
|
|
273
|
+
if (removeResult.exitCode !== 0) {
|
|
274
|
+
warnings.push(`worktree remove: ${removeResult.stderr}`);
|
|
275
|
+
}
|
|
276
|
+
} else {
|
|
277
|
+
warnings.push(`worktree remove: ${toPosix(worktreePath)} is not a registered worktree (already removed?)`);
|
|
236
278
|
}
|
|
237
279
|
|
|
238
280
|
// Branch cleanup — only if branch still exists.
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
* findings record|list|dispose|debt Judges' verdicts and their findings ledger (evidence loop)
|
|
32
32
|
* phase-plan-index <phase> Index plans with waves and status
|
|
33
33
|
* websearch <query> Search web via Brave API (if configured)
|
|
34
|
-
* [--limit N] [--freshness day|week|month]
|
|
34
|
+
* [--limit N] [--freshness day|week|month|year]
|
|
35
35
|
*
|
|
36
36
|
* Phase Listing:
|
|
37
37
|
* phases list [--type plans|summaries] List phases with optional type filter
|
|
@@ -237,6 +237,9 @@ const costRebuild = require('./lib/cost-rebuild.cjs');
|
|
|
237
237
|
const preview = require('./lib/preview.cjs');
|
|
238
238
|
const reviewDeep = require('./lib/review-deep.cjs');
|
|
239
239
|
const findings = require('./lib/findings.cjs');
|
|
240
|
+
const verifyPlanSet = require('./lib/verify-plan-set.cjs');
|
|
241
|
+
const verifyIntegrity = require('./lib/verify-integrity.cjs');
|
|
242
|
+
const phaseGate = require('./lib/phase-gate.cjs');
|
|
240
243
|
const knowledge = require('./lib/knowledge.cjs');
|
|
241
244
|
const skillAlign = require('./lib/skill-align.cjs');
|
|
242
245
|
const hygiene = require('./lib/hygiene.cjs');
|
|
@@ -649,8 +652,12 @@ async function main() {
|
|
|
649
652
|
verify.cmdVerifyScope(cwd, args[2], raw);
|
|
650
653
|
} else if (subcommand === 'stale') {
|
|
651
654
|
verify.cmdVerifyStale(cwd, args[2], raw);
|
|
655
|
+
} else if (subcommand === 'plan-set') {
|
|
656
|
+
verifyPlanSet.cmdVerifyPlanSet(cwd, args[2], raw);
|
|
657
|
+
} else if (subcommand === 'test-integrity') {
|
|
658
|
+
verifyIntegrity.cmdVerifyTestIntegrity(cwd, args[2], raw);
|
|
652
659
|
} else {
|
|
653
|
-
error('Unknown verify subcommand. Available: plan-structure, phase-completeness, references, commits, artifacts, key-links, reconcile, stubs, scope, stale');
|
|
660
|
+
error('Unknown verify subcommand. Available: plan-structure, phase-completeness, references, commits, artifacts, key-links, reconcile, stubs, scope, stale, plan-set, test-integrity');
|
|
654
661
|
}
|
|
655
662
|
break;
|
|
656
663
|
}
|
|
@@ -766,9 +773,11 @@ async function main() {
|
|
|
766
773
|
} else if (subcommand === 'complete') {
|
|
767
774
|
if (!args[2]) error('phase complete requires a phase number');
|
|
768
775
|
const noCommit = args.includes('--no-commit');
|
|
769
|
-
phase.cmdPhaseComplete(cwd, args[2], raw, { noCommit });
|
|
776
|
+
phase.cmdPhaseComplete(cwd, args[2], raw, { noCommit, force: args.includes('--force'), reason: getArgValue(args, '--reason') });
|
|
777
|
+
} else if (subcommand === 'gate') {
|
|
778
|
+
phaseGate.cmdPhaseGate(cwd, args[2], raw, { mode: getArgValue(args, '--mode') || undefined });
|
|
770
779
|
} else {
|
|
771
|
-
error('Unknown phase subcommand. Available: next-decimal, add, insert, remove, complete');
|
|
780
|
+
error('Unknown phase subcommand. Available: next-decimal, add, insert, remove, complete, gate');
|
|
772
781
|
}
|
|
773
782
|
break;
|
|
774
783
|
}
|
|
@@ -52,17 +52,28 @@ function writeJson(p, obj) { fs.mkdirSync(path.dirname(p), { recursive: true });
|
|
|
52
52
|
* Stage a merge request. Mints a fresh per-request approval token (nonce), records the
|
|
53
53
|
* exact branch + the (caller-attested) CI/verify flags, marks it awaiting_approval, and
|
|
54
54
|
* returns the record. Does NOT merge. The operator sets PAN_MERGE_APPROVAL to
|
|
55
|
-
* `record.approval_token` after reviewing.
|
|
56
|
-
*
|
|
55
|
+
* `record.approval_token` after reviewing. ci_green is agent-attested advisory context.
|
|
56
|
+
* verify_pass is too, unless the request names a `phase`: then it is the phase gate's
|
|
57
|
+
* verdict (program V1), computed here, and the caller's flag is ignored. The token is
|
|
58
|
+
* the sole unfakeable anchor either way.
|
|
57
59
|
*/
|
|
58
60
|
function requestMerge(cwd, opts = {}) {
|
|
59
61
|
const branch = validateBranch(opts.branch);
|
|
62
|
+
let verifyPass = !!opts.verify_pass;
|
|
63
|
+
let gate = null;
|
|
64
|
+
if (opts.phase != null && String(opts.phase).trim() !== '') {
|
|
65
|
+
const g = require('../bin/lib/phase-gate.cjs').phaseGate(cwd, String(opts.phase).trim());
|
|
66
|
+
gate = g.error ? { phase: String(opts.phase), error: g.error } : { phase: g.phase, ok: g.ok, reasons: g.reasons.map((r) => r.code) };
|
|
67
|
+
verifyPass = !g.error && g.ok;
|
|
68
|
+
}
|
|
60
69
|
const record = {
|
|
61
70
|
id: slugify(branch),
|
|
62
71
|
branch,
|
|
63
72
|
approval_token: crypto.randomBytes(9).toString('hex'),
|
|
64
73
|
ci_green: !!opts.ci_green,
|
|
65
|
-
verify_pass:
|
|
74
|
+
verify_pass: verifyPass,
|
|
75
|
+
verify_source: gate ? 'phase_gate' : 'caller',
|
|
76
|
+
...(gate ? { gate } : {}),
|
|
66
77
|
status: 'awaiting_approval',
|
|
67
78
|
requested_at: opts.now || null,
|
|
68
79
|
};
|
|
@@ -126,14 +126,14 @@ const NATIVE_TOOLS = [
|
|
|
126
126
|
{
|
|
127
127
|
name: 'pan_request_merge',
|
|
128
128
|
title: 'Request a gated merge',
|
|
129
|
-
description: 'Stage a squash-merge request for a branch and mark it awaiting human approval. Records intent only — does NOT merge.',
|
|
129
|
+
description: 'Stage a squash-merge request for a branch and mark it awaiting human approval. Records intent only — does NOT merge. Name the phase the branch delivers: verify_pass is then the phase gate\'s verdict, not the caller\'s flag.',
|
|
130
130
|
readOnly: false, destructive: false,
|
|
131
131
|
inputSchema: {
|
|
132
132
|
type: 'object', additionalProperties: false, required: ['branch'],
|
|
133
|
-
properties: { branch: { type: 'string' }, ci_green: { type: 'boolean' }, verify_pass: { type: 'boolean' } },
|
|
133
|
+
properties: { branch: { type: 'string' }, ci_green: { type: 'boolean' }, verify_pass: { type: 'boolean' }, phase: { type: 'string' } },
|
|
134
134
|
},
|
|
135
135
|
handler: ({ cwd, input }) => ({
|
|
136
|
-
json: mergeGate.requestMerge(cwd, { branch: input.branch, ci_green: input.ci_green, verify_pass: input.verify_pass }),
|
|
136
|
+
json: mergeGate.requestMerge(cwd, { branch: input.branch, ci_green: input.ci_green, verify_pass: input.verify_pass, phase: input.phase }),
|
|
137
137
|
}),
|
|
138
138
|
},
|
|
139
139
|
{
|
|
@@ -11,8 +11,9 @@
|
|
|
11
11
|
// hooks/pan-state-reinject.js after compaction.
|
|
12
12
|
//
|
|
13
13
|
// Installed by the PAN installer as <opencode config dir>/plugins/pan-wizard.js,
|
|
14
|
-
//
|
|
15
|
-
// CommonJS and exports
|
|
14
|
+
// below the `{"type":"commonjs"}` package.json PAN writes in the config dir, so this file is
|
|
15
|
+
// CommonJS and exports one object both loaders accept: `{ id, server, setup }`
|
|
16
|
+
// (OpenCode 1 calls server(), OpenCode 2 calls setup()).
|
|
16
17
|
//
|
|
17
18
|
// Inert unless the project is a PAN project with work in flight (a state.md naming
|
|
18
19
|
// a current phase and a roadmap.md with an unticked phase). Never writes anything.
|
|
@@ -96,16 +97,43 @@ function compactionContext(directory) {
|
|
|
96
97
|
return block.length > BLOCK_MAX ? block.slice(0, BLOCK_MAX - 1) + '…' : block;
|
|
97
98
|
}
|
|
98
99
|
|
|
100
|
+
function directoryOf(ctx) {
|
|
101
|
+
return (ctx && (ctx.directory || ctx.worktree)) || process.cwd();
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** Push PAN's block onto the first array target; fail-open, inert outside work in flight. */
|
|
105
|
+
function pushBlock(directory, target) {
|
|
106
|
+
try {
|
|
107
|
+
if (!Array.isArray(target)) return;
|
|
108
|
+
const block = compactionContext(directory);
|
|
109
|
+
if (block) target.push(block);
|
|
110
|
+
} catch { /* fail open: a compaction without PAN's block beats a broken one */ }
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// OpenCode 1: the loader calls server() and reads its hook map.
|
|
99
114
|
async function server(ctx) {
|
|
100
|
-
const directory = (ctx
|
|
115
|
+
const directory = directoryOf(ctx);
|
|
101
116
|
return {
|
|
102
117
|
'experimental.session.compacting': async (input, output) => {
|
|
103
|
-
|
|
104
|
-
const block = compactionContext(directory);
|
|
105
|
-
if (block && output && Array.isArray(output.context)) output.context.push(block);
|
|
106
|
-
} catch { /* fail open: a compaction without PAN's block beats a broken one */ }
|
|
118
|
+
pushBlock(directory, output && output.context);
|
|
107
119
|
},
|
|
108
120
|
};
|
|
109
121
|
}
|
|
110
122
|
|
|
111
|
-
|
|
123
|
+
// OpenCode 2: the loader requires `setup` (or `effect`) and hooks register through
|
|
124
|
+
// `ctx.session.hook(name, fn)` (opencode.ai/v2/docs/build/plugins, migrate-v1). The
|
|
125
|
+
// compaction event's prompt parts are `system` (read from the 2.0 binary); `context`
|
|
126
|
+
// is the documented v1 name and the fallback.
|
|
127
|
+
function setup(ctx) {
|
|
128
|
+
try {
|
|
129
|
+
const directory = directoryOf(ctx);
|
|
130
|
+
const session = ctx && ctx.session;
|
|
131
|
+
if (!session || typeof session.hook !== 'function') return;
|
|
132
|
+
session.hook('compaction', async (event) => {
|
|
133
|
+
if (!event) return;
|
|
134
|
+
pushBlock(directory, Array.isArray(event.system) ? event.system : event.context);
|
|
135
|
+
});
|
|
136
|
+
} catch { /* fail open: a plugin that cannot register leaves the session as it was */ }
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
module.exports = { id: 'pan-wizard', server, setup };
|
|
@@ -34,7 +34,7 @@ PAN never selects your host model — it recommends a *class*. Because the `reas
|
|
|
34
34
|
|
|
35
35
|
The IDs below are *examples* of what you can configure in each class — not an exhaustive or only-valid list. A newer release in the same class behaves the same way here.
|
|
36
36
|
|
|
37
|
-
At install time PAN also runs a **best-effort, advisory** capability check on the model ID you have configured (`detectModelCapabilities` in `bin/install-lib.cjs`). It recognizes the current Claude generations by name, and its forward threshold per family is the last reduced-capability release it records rather than the newest release it lists: any Claude ID newer than that boundary — a new major, or a later point release inside a major it already lists — inherits that family's modern profile instead of reporting no capabilities at all. "That family's profile" is the point: a newer Haiku inherits Haiku
|
|
37
|
+
At install time PAN also runs a **best-effort, advisory** capability check on the model ID you have configured (`detectModelCapabilities` in `bin/install-lib.cjs`). It recognizes the current Claude generations by name, and its forward threshold per family is the last reduced-capability release it records rather than the newest release it lists: any Claude ID newer than that boundary — a new major, or a later point release inside a major it already lists — inherits that family's modern profile instead of reporting no capabilities at all. "That family's profile" is the point: a newer Haiku inherits the newest Haiku profile the table records (1M context and thinking, at the fast tier), not Opus's. An ID resolves either by matching an explicit branch in the table or — for Claude names — by carrying a family plus a readable release number strictly newer than that family's last reduced-capability release; anything that resolves neither way reads as `tier: 'unknown'` with every capability flag false. It is a name-matching heuristic, not a live probe of the model, so it can still be imprecise about something released after this table — read it as a hint, not a contract. Either way it decides one thing only: whether the installer prints a capability *warning*. No routing, mode-selection, or feature gate in PAN reads it, so a wrong guess costs you a misleading install-time line and nothing else.
|
|
38
38
|
|
|
39
39
|
| Class | Example model IDs | Role in PAN | Context | Relative cost | Notes |
|
|
40
40
|
|-------|-------------------|-------------|---------|---------------|-------|
|
|
@@ -42,7 +42,7 @@ node ~/.claude/pan-wizard-core/bin/pan-tools.cjs commit "docs: update state" --f
|
|
|
42
42
|
|
|
43
43
|
# Load config via state load (returns JSON):
|
|
44
44
|
INIT=$(node ~/.claude/pan-wizard-core/bin/pan-tools.cjs state load)
|
|
45
|
-
# commit_docs is
|
|
45
|
+
# commit_docs is at .config.commit_docs in the JSON output
|
|
46
46
|
|
|
47
47
|
# Or use init commands which include commit_docs:
|
|
48
48
|
INIT=$(node ~/.claude/pan-wizard-core/bin/pan-tools.cjs init execute-phase "1")
|
|
@@ -147,7 +147,7 @@ INIT=$(node ~/.claude/pan-wizard-core/bin/pan-tools.cjs init execute-phase "1")
|
|
|
147
147
|
Or use `state load` for the config values:
|
|
148
148
|
```bash
|
|
149
149
|
INIT=$(node ~/.claude/pan-wizard-core/bin/pan-tools.cjs state load)
|
|
150
|
-
# Parse branching_strategy, phase_branch_template, milestone_branch_template from JSON
|
|
150
|
+
# Parse .config.branching_strategy, .config.phase_branch_template, .config.milestone_branch_template from JSON
|
|
151
151
|
```
|
|
152
152
|
|
|
153
153
|
**Branch creation:**
|
|
@@ -168,7 +168,7 @@ if [ "$BRANCHING_STRATEGY" = "milestone" ]; then
|
|
|
168
168
|
fi
|
|
169
169
|
```
|
|
170
170
|
|
|
171
|
-
**Merge options
|
|
171
|
+
**Merge options after milestone-done (it does not merge — run one yourself):**
|
|
172
172
|
|
|
173
173
|
| Option | Git command | Result |
|
|
174
174
|
|--------|-------------|--------|
|
|
@@ -26,6 +26,7 @@ A judging agent ends its report with one fenced `pan-verdict` block. The block i
|
|
|
26
26
|
| `verdict` | yes | your own verdict word (table below) |
|
|
27
27
|
| `outcome` | yes | `pass`, `fail` or `needs_human` |
|
|
28
28
|
| `findings` | yes (may be empty) | a list of `{class, severity, where, summary}` |
|
|
29
|
+
| `resolutions` | on a re-check | a list of `{id, status, evidence}` answering each earlier open finding you were given in `<open_findings>`: `status` is `resolved` (with `evidence`: what you checked and saw), `still_present` or `cannot_tell`. A finding you leave out stays open, and `resolved` without evidence is recorded as `cannot_tell` |
|
|
29
30
|
| `not_checked` | when a check could not run | a list of `{check, reason}`: a check you were asked to make and could not run, and why (no test script, no browser, a file you could not read). Never fold it into `pass`: a verdict with entries here says what it did not see |
|
|
30
31
|
|
|
31
32
|
| Agent | `verdict` → `outcome` |
|
|
@@ -36,6 +37,10 @@ A judging agent ends its report with one fenced `pan-verdict` block. The block i
|
|
|
36
37
|
|
|
37
38
|
The verifier does not emit this block. Its verification.md frontmatter (`status`, `gaps`, `human_verification`, `unrequested`, `test_gate`, `not_checked`) is its machine contract, and the recorder reads that directly. `test_gate` is `passed`, `failed` or `skipped`. A skipped gate is recorded as a `tests` entry in `not_checked` even when the list leaves it out, and a verification that does not record `test_gate` at all is recorded with a `test_gate_unrecorded` warning.
|
|
38
39
|
|
|
40
|
+
**Findings close on evidence, never on silence.** An earlier finding closes only when the judge answers it `resolved` with evidence (a `resolutions` entry, or a truth in the verifier's `re_verification.gaps_closed`), or, for a `code-fact:` finding, when that fact passes. A finding the new report neither repeats nor answers stays open and is listed as `unaddressed`. A reworded report of an earlier finding (same class family, where `missing` and `partial` count as one; same file; same truth or a similar summary) keeps the earlier id.
|
|
41
|
+
|
|
42
|
+
**Code facts only lower a verdict (two-key verdict).** A fact the engine measures can turn a judge's `pass` or `needs_human` into a fail, never the reverse. `test_gate: failed` under `status: passed` records as `gaps_found`: the record keeps the judge's own word as `agent_verdict`, names the fact in `lowered_by`, and adds a high finding at `code-fact:test_gate` for the gap round to close. `findings record --raw` prints the lowered word, so the workflow routes to gap closure.
|
|
43
|
+
|
|
39
44
|
## `class`: what kind of finding
|
|
40
45
|
|
|
41
46
|
| Class | Use when |
|
|
@@ -67,7 +72,7 @@ The verifier does not emit this block. Its verification.md frontmatter (`status`
|
|
|
67
72
|
- `where`: `NN-MM-plan.md` or `NN-MM-plan.md#task-N`.
|
|
68
73
|
|
|
69
74
|
**pan-reviewer.**
|
|
70
|
-
- Classes by category: Convention → `quality`, Security → `risk`, Code Quality → `defect`, work no plan asked for → `unrequested`.
|
|
75
|
+
- Classes by category: Convention → `quality`, Security → `risk`, Code Quality → `defect`, Design smell → `quality`, work no plan asked for → `unrequested`.
|
|
71
76
|
- Severity: ERROR → `high`, WARNING → `medium`, INFO → `info`.
|
|
72
77
|
- `where`: `path:line`.
|
|
73
78
|
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
"auto_advance": false,
|
|
9
9
|
"nyquist_validation": false,
|
|
10
10
|
"phase_record_compact": false,
|
|
11
|
+
"phase_gate": "warn",
|
|
11
12
|
"phase_reports": { "enabled": false, "open": false, "theme": "auto", "index": true }
|
|
12
13
|
},
|
|
13
14
|
"planning": {
|
|
@@ -15,12 +16,7 @@
|
|
|
15
16
|
"search_gitignored": false
|
|
16
17
|
},
|
|
17
18
|
"parallelization": {
|
|
18
|
-
"enabled": true
|
|
19
|
-
"plan_level": true,
|
|
20
|
-
"task_level": false,
|
|
21
|
-
"skip_checkpoints": true,
|
|
22
|
-
"max_concurrent_agents": 3,
|
|
23
|
-
"min_plans_for_parallel": 2
|
|
19
|
+
"enabled": true
|
|
24
20
|
},
|
|
25
21
|
"gates": {
|
|
26
22
|
"confirm_project": true,
|
|
@@ -57,13 +57,14 @@ Create: .planning/debug/{slug}.md
|
|
|
57
57
|
Task(
|
|
58
58
|
prompt=filled_template,
|
|
59
59
|
subagent_type="pan-debugger",
|
|
60
|
+
model="{debugger_model}",
|
|
60
61
|
description="Debug {slug}"
|
|
61
62
|
)
|
|
62
63
|
```
|
|
63
64
|
|
|
64
65
|
**From diagnose-issues (UAT):**
|
|
65
66
|
```python
|
|
66
|
-
Task(prompt=template, subagent_type="pan-debugger", description="Debug UAT-001")
|
|
67
|
+
Task(prompt=template, subagent_type="pan-debugger", model="{debugger_model}", description="Debug UAT-001")
|
|
67
68
|
```
|
|
68
69
|
|
|
69
70
|
---
|
|
@@ -22,6 +22,7 @@ files_modified: []
|
|
|
22
22
|
autonomous: true
|
|
23
23
|
requirements: []
|
|
24
24
|
user_setup: []
|
|
25
|
+
planned_commit: <sha>
|
|
25
26
|
|
|
26
27
|
# Goal-backward verification (derived during planning, verified after execution); the Frontmatter Fields table below explains each field
|
|
27
28
|
must_haves:
|
|
@@ -144,6 +145,7 @@ After completion, create `.planning/phases/XX-name/{phase}-{plan}-summary.md`
|
|
|
144
145
|
| `files_modified` | Yes | Files this plan touches. |
|
|
145
146
|
| `autonomous` | Yes | `true` if no checkpoints, `false` if has checkpoints |
|
|
146
147
|
| `requirements` | Yes | **MUST** list requirement IDs from ROADMAP. Every roadmap requirement MUST appear in at least one plan. |
|
|
148
|
+
| `planned_commit` | Yes | `git rev-parse HEAD` when the plan is written. exec-phase lists the `files_modified` that changed on the branch since then, so an executor knows its plan's picture of the code may be out of date. |
|
|
147
149
|
| `user_setup` | No | Array of human-required setup items (external services) |
|
|
148
150
|
| `must_haves` | Yes | Goal-backward verification criteria (see below) |
|
|
149
151
|
|
|
@@ -71,6 +71,7 @@ Before returning PLANNING COMPLETE:
|
|
|
71
71
|
Task(
|
|
72
72
|
prompt=filled_template,
|
|
73
73
|
subagent_type="pan-planner",
|
|
74
|
+
model="{planner_model}",
|
|
74
75
|
description="Plan Phase {phase}"
|
|
75
76
|
)
|
|
76
77
|
```
|
|
@@ -80,6 +81,7 @@ Task(
|
|
|
80
81
|
Task(
|
|
81
82
|
prompt=filled_template, # with mode: gap_closure
|
|
82
83
|
subagent_type="pan-planner",
|
|
84
|
+
model="{planner_model}",
|
|
83
85
|
description="Plan gaps for Phase {phase}"
|
|
84
86
|
)
|
|
85
87
|
```
|