pan-wizard 3.22.0 → 3.24.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 +38 -24
- package/agents/pan-conductor.md +3 -3
- package/agents/pan-design-checker.md +83 -0
- package/agents/pan-designer.md +67 -0
- package/agents/pan-document_code.md +3 -2
- package/agents/pan-executor.md +2 -2
- package/agents/pan-plan-checker.md +19 -6
- package/agents/pan-planner.md +4 -1
- package/agents/pan-previewer.md +1 -1
- package/agents/pan-roadmapper.md +3 -1
- package/agents/pan-verifier.md +10 -10
- package/bin/install-lib.cjs +128 -17
- package/bin/install.js +480 -74
- package/commands/pan/army.md +20 -14
- package/commands/pan/audit-deployment.md +17 -14
- package/commands/pan/cost.md +2 -2
- package/commands/pan/debug.md +1 -1
- package/commands/pan/design-phase.md +77 -0
- package/commands/pan/exec-phase.md +2 -2
- package/commands/pan/experiment.md +18 -14
- package/commands/pan/focus-auto.md +5 -5
- package/commands/pan/focus-design.md +13 -1
- package/commands/pan/focus-exec.md +1 -1
- package/commands/pan/learn.md +1 -1
- package/commands/pan/links.md +3 -1
- package/commands/pan/map-codebase.md +4 -4
- package/commands/pan/mcp-bridge.md +1 -1
- package/commands/pan/milestone-new.md +3 -1
- package/commands/pan/optimize.md +2 -2
- package/commands/pan/patches.md +1 -1
- package/commands/pan/plan-phase.md +3 -1
- package/commands/pan/preview.md +3 -3
- package/commands/pan/profile.md +3 -3
- package/commands/pan/research-phase.md +1 -1
- package/commands/pan/retro.md +4 -1
- package/commands/pan/review-deep.md +1 -1
- package/commands/pan/settings.md +2 -2
- package/commands/pan/what-if.md +1 -1
- package/hooks/dist/pan-check-update.js +149 -38
- package/hooks/dist/pan-context-monitor.js +155 -81
- package/hooks/dist/pan-cost-logger.js +288 -13
- package/hooks/dist/pan-statusline.js +24 -8
- package/hooks/dist/pan-stop-guard.js +160 -0
- package/hooks/dist/pan-trace-logger.js +261 -19
- package/package.json +2 -2
- package/pan-wizard-core/bin/lib/bridge.cjs +1 -1
- package/pan-wizard-core/bin/lib/bus.cjs +19 -4
- package/pan-wizard-core/bin/lib/campaign.cjs +8 -3
- package/pan-wizard-core/bin/lib/codebase.cjs +15 -5
- package/pan-wizard-core/bin/lib/commands.cjs +88 -6
- package/pan-wizard-core/bin/lib/config.cjs +27 -2
- package/pan-wizard-core/bin/lib/constants.cjs +1 -0
- package/pan-wizard-core/bin/lib/core.cjs +113 -8
- package/pan-wizard-core/bin/lib/cost.cjs +61 -24
- package/pan-wizard-core/bin/lib/distill.cjs +22 -4
- package/pan-wizard-core/bin/lib/doc-lint.cjs +25 -7
- package/pan-wizard-core/bin/lib/experiment.cjs +25 -1
- package/pan-wizard-core/bin/lib/focus.cjs +42 -7
- package/pan-wizard-core/bin/lib/frontmatter.cjs +62 -31
- package/pan-wizard-core/bin/lib/git.cjs +51 -16
- package/pan-wizard-core/bin/lib/hud.cjs +17 -2
- package/pan-wizard-core/bin/lib/init.cjs +2 -5
- package/pan-wizard-core/bin/lib/knowledge.cjs +20 -1
- package/pan-wizard-core/bin/lib/learn-lint.cjs +50 -0
- package/pan-wizard-core/bin/lib/links.cjs +9 -6
- package/pan-wizard-core/bin/lib/lock.cjs +23 -4
- package/pan-wizard-core/bin/lib/memory-optimize.cjs +11 -6
- package/pan-wizard-core/bin/lib/milestone.cjs +11 -5
- package/pan-wizard-core/bin/lib/optimize.cjs +18 -7
- package/pan-wizard-core/bin/lib/phase-remove.cjs +41 -2
- package/pan-wizard-core/bin/lib/phase.cjs +82 -11
- package/pan-wizard-core/bin/lib/preview.cjs +23 -4
- package/pan-wizard-core/bin/lib/review-deep.cjs +14 -6
- package/pan-wizard-core/bin/lib/roadmap.cjs +6 -2
- package/pan-wizard-core/bin/lib/runner.cjs +17 -7
- package/pan-wizard-core/bin/lib/squads.cjs +43 -11
- package/pan-wizard-core/bin/lib/state.cjs +32 -11
- package/pan-wizard-core/bin/lib/template.cjs +8 -3
- package/pan-wizard-core/bin/lib/verify-deploy.cjs +14 -2
- package/pan-wizard-core/bin/lib/verify-drift.cjs +15 -1
- package/pan-wizard-core/bin/lib/verify-preflight.cjs +9 -3
- package/pan-wizard-core/bin/lib/verify.cjs +84 -39
- package/pan-wizard-core/bin/lib/whatif.cjs +8 -1
- package/pan-wizard-core/bin/pan-tools.cjs +42 -19
- package/pan-wizard-core/learnings/index.json +7 -7
- package/pan-wizard-core/learnings/universal/autonomous-loop.md +5 -3
- package/pan-wizard-core/learnings/universal/concurrency.md +1 -1
- package/pan-wizard-core/references/design-methodology.md +94 -0
- package/pan-wizard-core/references/git-integration.md +9 -9
- package/pan-wizard-core/references/guardrails.md +4 -1
- package/pan-wizard-core/references/model-profile-resolution.md +1 -1
- package/pan-wizard-core/references/model-profiles.md +44 -34
- package/pan-wizard-core/references/planning-config.md +5 -5
- package/pan-wizard-core/references/verification-patterns.md +67 -0
- package/pan-wizard-core/templates/codebase/architecture.md +1 -1
- package/pan-wizard-core/templates/codebase/concerns.md +1 -1
- package/pan-wizard-core/templates/codebase/conventions.md +1 -1
- package/pan-wizard-core/templates/codebase/integrations.md +1 -1
- package/pan-wizard-core/templates/codebase/stack.md +1 -1
- package/pan-wizard-core/templates/codebase/structure.md +1 -1
- package/pan-wizard-core/templates/codebase/testing.md +1 -1
- package/pan-wizard-core/templates/design.md +146 -0
- package/pan-wizard-core/templates/discovery.md +3 -3
- package/pan-wizard-core/templates/milestone-archive.md +2 -2
- package/pan-wizard-core/templates/playbook.md +1 -1
- package/pan-wizard-core/templates/uat.md +1 -1
- package/pan-wizard-core/workflows/diagnose-issues.md +9 -7
- package/pan-wizard-core/workflows/exec-phase.md +28 -8
- package/pan-wizard-core/workflows/execute-plan.md +2 -3
- package/pan-wizard-core/workflows/health.md +24 -1
- package/pan-wizard-core/workflows/help.md +5 -4
- package/pan-wizard-core/workflows/learn.md +5 -5
- package/pan-wizard-core/workflows/map-codebase.md +1 -1
- package/pan-wizard-core/workflows/milestone-audit.md +1 -1
- package/pan-wizard-core/workflows/milestone-new.md +4 -4
- package/pan-wizard-core/workflows/new-project.md +47 -32
- package/pan-wizard-core/workflows/optimize.md +8 -8
- package/pan-wizard-core/workflows/pause.md +1 -1
- package/pan-wizard-core/workflows/phase-tests.md +2 -2
- package/pan-wizard-core/workflows/plan-phase.md +4 -0
- package/pan-wizard-core/workflows/profile.md +13 -10
- package/pan-wizard-core/workflows/research-phase.md +1 -1
- package/pan-wizard-core/workflows/resume-project.md +1 -1
- package/pan-wizard-core/workflows/settings.md +9 -9
- package/pan-wizard-core/workflows/transition.md +24 -8
- package/pan-wizard-core/workflows/update.md +23 -20
- package/pan-wizard-core/workflows/verify-phase.md +7 -2
- package/pan-zcode/README.md +12 -2
- package/pan-zcode/bin/install-zcode.js +1 -1
- package/pan-zcode/mcp/native-tools.cjs +1 -1
- package/pan-zcode/mcp/server.cjs +76 -14
- package/scripts/build-hooks.js +2 -1
- package/scripts/generate-skills-docs.py +0 -4
- package/scripts/run-tests.cjs +11 -4
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
const fs = require('fs');
|
|
8
8
|
const path = require('path');
|
|
9
|
-
const { safeReadFile, execGit, findPhaseInternal, output } = require('./core.cjs');
|
|
9
|
+
const { safeReadFile, execGit, findPhaseInternal, output, normalizePhaseName } = require('./core.cjs');
|
|
10
10
|
const { readStateSafe } = require('./state.cjs');
|
|
11
11
|
const {
|
|
12
12
|
STATE_FILE, ROADMAP_FILE, CONFIG_FILE, PATTERNS_FILE, PHASE_DIR_RE,
|
|
@@ -162,7 +162,13 @@ function cmdDepsValidate(cwd, raw) {
|
|
|
162
162
|
const headerRe = /#{2,4}\s*Phase\s+(\d+[A-Z]?(?:\.\d+)*)\s*:\s*([^\n]+)/gi;
|
|
163
163
|
let match;
|
|
164
164
|
while ((match = headerRe.exec(roadmapContent)) !== null) {
|
|
165
|
-
|
|
165
|
+
// Key both maps by the NORMALIZED number. Roadmap headings are unpadded
|
|
166
|
+
// ("Phase 1") and directory names are zero-padded ("01-foundation"), so
|
|
167
|
+
// keying on the raw values made every phase mismatch in BOTH directions:
|
|
168
|
+
// `deps validate` reported "in roadmap but no directory on disk" for phases
|
|
169
|
+
// whose directory was right there, plus an "orphaned directory" warning for
|
|
170
|
+
// the same phase — inventing errors on a roadmap in PAN's own template shape.
|
|
171
|
+
roadmapPhases.set(normalizePhaseName(match[1]), match[2].trim());
|
|
166
172
|
}
|
|
167
173
|
} else {
|
|
168
174
|
issues.push({ type: 'warning', message: 'roadmap.md not found' });
|
|
@@ -176,7 +182,7 @@ function cmdDepsValidate(cwd, raw) {
|
|
|
176
182
|
if (entry.isDirectory()) {
|
|
177
183
|
const dirMatch = entry.name.match(PHASE_DIR_RE);
|
|
178
184
|
if (dirMatch) {
|
|
179
|
-
diskPhases.set(dirMatch[1], entry.name);
|
|
185
|
+
diskPhases.set(normalizePhaseName(dirMatch[1]), entry.name);
|
|
180
186
|
}
|
|
181
187
|
}
|
|
182
188
|
}
|
|
@@ -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, execGit, findPhaseInternal, getMilestoneInfo, toPosix, output, error, escapeRegex } = require('./core.cjs');
|
|
8
|
+
const { safeReadFile, normalizePhaseName, comparePhaseNum, execGit, findPhaseInternal, getMilestoneInfo, toPosix, output, EXIT_OK, error, escapeRegex } = require('./core.cjs');
|
|
9
9
|
const { extractFrontmatter, parseMustHavesBlock } = require('./frontmatter.cjs');
|
|
10
10
|
const { writeStateMd, readStateSafe } = require('./state.cjs');
|
|
11
11
|
const {
|
|
@@ -362,7 +362,17 @@ function cmdVerifyArtifacts(cwd, planFilePath, raw) {
|
|
|
362
362
|
if (!content) { output({ error: 'File not found', path: planFilePath }, raw); return; }
|
|
363
363
|
const r = checkArtifacts(cwd, content);
|
|
364
364
|
if (r.total === 0) {
|
|
365
|
-
|
|
365
|
+
// EXIT_OK: "nothing declared to check" is a RESULT, not a failure. Two pieces of
|
|
366
|
+
// shipped evidence: (1) the plan template ships `must_haves: { truths: [],
|
|
367
|
+
// artifacts: [], key_links: [] }` (pan-wizard-core/templates/phase-prompt.md,
|
|
368
|
+
// template.cjs generatePlanTemplate), so an empty block is the documented default
|
|
369
|
+
// state of a scaffolded plan; presence of the block is enforced by
|
|
370
|
+
// `verify plan-structure`, not here. (2) parseMustHavesBlock currently matches the
|
|
371
|
+
// block header at 4-space indent while every shipped template and agent emits it at
|
|
372
|
+
// 2 — so this branch is reached for *every* plan authored in PAN's own format, and
|
|
373
|
+
// exiting non-zero would fail every real `verify artifacts` call. Reclassifying
|
|
374
|
+
// this as a failure requires fixing that indentation mismatch first.
|
|
375
|
+
output({ error: 'No must_haves.artifacts found in frontmatter', path: planFilePath }, raw, undefined, EXIT_OK);
|
|
366
376
|
return;
|
|
367
377
|
}
|
|
368
378
|
output(r, raw, r.all_passed ? 'valid' : 'invalid');
|
|
@@ -425,7 +435,10 @@ function cmdVerifyKeyLinks(cwd, planFilePath, raw) {
|
|
|
425
435
|
if (!content) { output({ error: 'File not found', path: planFilePath }, raw); return; }
|
|
426
436
|
const r = checkKeyLinks(cwd, content);
|
|
427
437
|
if (r.total === 0) {
|
|
428
|
-
|
|
438
|
+
// EXIT_OK: same reasoning as cmdVerifyArtifacts above — an empty key_links block is
|
|
439
|
+
// the shipped template default, and the parseMustHavesBlock indent mismatch means
|
|
440
|
+
// this branch fires for every plan written in PAN's own format.
|
|
441
|
+
output({ error: 'No must_haves.key_links found in frontmatter', path: planFilePath }, raw, undefined, EXIT_OK);
|
|
429
442
|
return;
|
|
430
443
|
}
|
|
431
444
|
output(r, raw, r.all_verified ? 'valid' : 'invalid');
|
|
@@ -490,8 +503,11 @@ function reconcilePhase(cwd, phaseNum) {
|
|
|
490
503
|
function cmdVerifyReconcile(cwd, phaseNum, raw) {
|
|
491
504
|
if (!phaseNum) { error('Usage: verify reconcile <phase>'); }
|
|
492
505
|
const r = reconcilePhase(cwd, phaseNum);
|
|
493
|
-
|
|
494
|
-
|
|
506
|
+
// Exit non-zero on contradiction so exec-phase's auto-advance gate actually
|
|
507
|
+
// stops on a rubber-stamped verification. output() previously hard-coded
|
|
508
|
+
// exit 0, making the old trailing process.exit dead code (H3, ADR audit
|
|
509
|
+
// 2026-08); the exitCode arg restores the gate.
|
|
510
|
+
output(r, raw, r.reconciled ? 'valid' : 'invalid', r.reconciled ? 0 : 1);
|
|
495
511
|
}
|
|
496
512
|
|
|
497
513
|
// ─── Stub / fake-return scanner (ADR-0036 review — closes the hardcoded
|
|
@@ -516,7 +532,9 @@ const STUB_CODE_EXT = /\.(js|cjs|mjs|jsx|ts|tsx|py|go|rb|java|php|rs|c|cc|cpp|h|
|
|
|
516
532
|
* @returns {{scanned, findings: Array, blocking: number, total: number}}
|
|
517
533
|
*/
|
|
518
534
|
function scanStubs(cwd, opts = {}) {
|
|
519
|
-
|
|
535
|
+
// includeUntracked: a stub gate that cannot see new files is not a gate. See the
|
|
536
|
+
// note in verify-drift.cjs getChangedFiles for why drift does not want the same.
|
|
537
|
+
let files = Array.isArray(opts.files) ? opts.files : getChangedFiles(cwd, null, { includeUntracked: true });
|
|
520
538
|
files = (files || []).filter(f => STUB_CODE_EXT.test(f));
|
|
521
539
|
const findings = [];
|
|
522
540
|
for (const rel of files) {
|
|
@@ -538,8 +556,9 @@ function scanStubs(cwd, opts = {}) {
|
|
|
538
556
|
|
|
539
557
|
function cmdVerifyStubs(cwd, opts = {}, raw) {
|
|
540
558
|
const r = scanStubs(cwd, opts);
|
|
541
|
-
|
|
542
|
-
|
|
559
|
+
// --gate must exit non-zero on blocking findings; output() used to hard-exit 0
|
|
560
|
+
// before the gate check, so the gate was dead (M30, ADR audit 2026-08).
|
|
561
|
+
output(r, raw, r.blocking === 0 ? 'valid' : 'invalid', opts.gate ? (r.blocking > 0 ? 1 : 0) : 0);
|
|
543
562
|
}
|
|
544
563
|
|
|
545
564
|
/**
|
|
@@ -757,8 +776,17 @@ function checkStateFile(cwd, addIssue, repairs) {
|
|
|
757
776
|
if (stateContent === null) {
|
|
758
777
|
// skip further state checks
|
|
759
778
|
} else {
|
|
760
|
-
// Extract phase references (e.g. "Phase 3" or "phase 01") from state.md
|
|
761
|
-
|
|
779
|
+
// Extract phase references (e.g. "Phase 3" or "phase 01") from state.md,
|
|
780
|
+
// deduped by normalized phase number. A real state.md mentions the same phase
|
|
781
|
+
// several times -- a total, a progress line, a decisions note -- and one missing
|
|
782
|
+
// directory must produce one warning, not one per mention. Keying on the
|
|
783
|
+
// normalized form collapses "Phase 3" and "Phase 03"; the first spelling seen
|
|
784
|
+
// is kept so the message quotes what the file actually says.
|
|
785
|
+
const phaseRefs = new Map();
|
|
786
|
+
for (const match of stateContent.matchAll(/[Pp]hase\s+(\d+(?:\.\d+)*)/g)) {
|
|
787
|
+
const key = normalizePhaseName(match[1]);
|
|
788
|
+
if (!phaseRefs.has(key)) phaseRefs.set(key, match[1]);
|
|
789
|
+
}
|
|
762
790
|
// Get disk phases for cross-reference
|
|
763
791
|
const diskPhases = new Set();
|
|
764
792
|
try {
|
|
@@ -773,7 +801,7 @@ function checkStateFile(cwd, addIssue, repairs) {
|
|
|
773
801
|
// phases/ directory may not exist yet
|
|
774
802
|
}
|
|
775
803
|
// Check for invalid references -- only warn if there are phases on disk
|
|
776
|
-
for (const ref of phaseRefs) {
|
|
804
|
+
for (const ref of phaseRefs.values()) {
|
|
777
805
|
const normalizedRef = String(parseInt(ref, 10)).padStart(2, '0');
|
|
778
806
|
if (!diskPhases.has(ref) && !diskPhases.has(normalizedRef) && !diskPhases.has(String(parseInt(ref, 10)))) {
|
|
779
807
|
if (diskPhases.size > 0) {
|
|
@@ -866,10 +894,28 @@ function crossCheckRoadmapDisk(cwd, phasesDirPath, addIssue) {
|
|
|
866
894
|
}
|
|
867
895
|
} catch { /* phases/ may not exist yet */ }
|
|
868
896
|
|
|
897
|
+
// A roadmap phase ahead of the current position has simply not been planned yet.
|
|
898
|
+
// PAN's own workflow creates the condition: /pan:new-project writes a roadmap
|
|
899
|
+
// declaring every phase up front, and /pan:plan-phase creates directories one at a
|
|
900
|
+
// time -- so warning on those would make `degraded` the permanent state of every
|
|
901
|
+
// multi-phase project and drain the verdict of signal. Only a phase at or behind
|
|
902
|
+
// the current position is genuinely suspect. With no readable current phase we
|
|
903
|
+
// cannot tell ahead from behind, so fall back to warning on everything.
|
|
904
|
+
let currentPhase = null;
|
|
905
|
+
try {
|
|
906
|
+
const stateContent = fs.readFileSync(path.join(planningPath(cwd), STATE_FILE), 'utf-8');
|
|
907
|
+
const m = stateContent.match(/\*\*Current Phase:\*\*\s*(\d+[A-Z]?(?:\.\d+)*)/i);
|
|
908
|
+
if (m) currentPhase = m[1];
|
|
909
|
+
} catch { /* no state.md -- checkStateFile reports that; stay conservative here */ }
|
|
910
|
+
|
|
869
911
|
for (const p of roadmapPhases) {
|
|
870
912
|
const padded = String(parseInt(p, 10)).padStart(2, '0');
|
|
871
|
-
if (
|
|
913
|
+
if (diskPhases.has(p) || diskPhases.has(padded)) continue;
|
|
914
|
+
if (currentPhase !== null && comparePhaseNum(p, currentPhase) > 0) {
|
|
915
|
+
addIssue('info', 'I002', `Phase ${p} in ${ROADMAP_FILE} is not planned yet (current phase: ${currentPhase})`, `Run /pan:plan-phase ${p} when you reach it`);
|
|
916
|
+
} else {
|
|
872
917
|
addIssue('warning', 'W006', `Phase ${p} in ${ROADMAP_FILE} but no directory on disk`, 'Create phase directory or remove from roadmap');
|
|
918
|
+
}
|
|
873
919
|
}
|
|
874
920
|
for (const p of diskPhases) {
|
|
875
921
|
const unpadded = String(parseInt(p, 10));
|
|
@@ -919,17 +965,13 @@ function repairIssues(cwd, repairs) {
|
|
|
919
965
|
switch (repair) {
|
|
920
966
|
case 'createConfig':
|
|
921
967
|
case 'resetConfig': {
|
|
922
|
-
// Write
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
plan_checker: true,
|
|
930
|
-
verifier: true,
|
|
931
|
-
parallelization: true,
|
|
932
|
-
};
|
|
968
|
+
// Write the canonical NESTED config via buildConfigDefaults. The old
|
|
969
|
+
// flat literal (research/plan_checker/verifier at top level) did NOT
|
|
970
|
+
// match the schema the gate reads (config.workflow.verifier), so
|
|
971
|
+
// --repair silently disabled the verification gate (M31, ADR audit
|
|
972
|
+
// 2026-08; note the flat form also drifted plan_checker vs plan_check).
|
|
973
|
+
const { buildConfigDefaults } = require('./config.cjs');
|
|
974
|
+
const defaults = buildConfigDefaults(false, {});
|
|
933
975
|
fs.writeFileSync(configFullPath, JSON.stringify(defaults, null, 2), 'utf-8');
|
|
934
976
|
repairActions.push({ action: repair, success: true, path: CONFIG_FILE });
|
|
935
977
|
break;
|
|
@@ -970,7 +1012,7 @@ function repairIssues(cwd, repairs) {
|
|
|
970
1012
|
}
|
|
971
1013
|
} catch (err) {
|
|
972
1014
|
// Repair action failed -- record the error so callers can report it
|
|
973
|
-
repairActions.push({ action: repair, success: false, error: err.message });
|
|
1015
|
+
repairActions.push({ action: repair, success: false, error: err.message || 'repair_failed' });
|
|
974
1016
|
}
|
|
975
1017
|
}
|
|
976
1018
|
|
|
@@ -1111,7 +1153,7 @@ function syncRequirementCheckboxes(cwd) {
|
|
|
1111
1153
|
}
|
|
1112
1154
|
|
|
1113
1155
|
if (fixed > 0) {
|
|
1114
|
-
try { fs.writeFileSync(reqPath, reqContent, 'utf-8'); } catch (e) { return { fixed: 0, error: e.message }; }
|
|
1156
|
+
try { fs.writeFileSync(reqPath, reqContent, 'utf-8'); } catch (e) { return { fixed: 0, error: e.message || 'requirements_write_failed' }; }
|
|
1115
1157
|
}
|
|
1116
1158
|
return { fixed };
|
|
1117
1159
|
}
|
|
@@ -1161,7 +1203,7 @@ function syncRoadmapPlanCheckboxes(cwd) {
|
|
|
1161
1203
|
}
|
|
1162
1204
|
|
|
1163
1205
|
if (fixed > 0) {
|
|
1164
|
-
try { fs.writeFileSync(roadmapPath, roadmapContent, 'utf-8'); } catch (e) { return { fixed: 0, error: e.message }; }
|
|
1206
|
+
try { fs.writeFileSync(roadmapPath, roadmapContent, 'utf-8'); } catch (e) { return { fixed: 0, error: e.message || 'roadmap_write_failed' }; }
|
|
1165
1207
|
}
|
|
1166
1208
|
return { fixed };
|
|
1167
1209
|
}
|
|
@@ -1308,19 +1350,6 @@ function cmdValidateHealth(cwd, options, raw) {
|
|
|
1308
1350
|
}
|
|
1309
1351
|
}
|
|
1310
1352
|
|
|
1311
|
-
// Determine overall status from error/warning counts
|
|
1312
|
-
let status;
|
|
1313
|
-
if (errors.length > 0) {
|
|
1314
|
-
status = HEALTH_STATUS.BROKEN;
|
|
1315
|
-
} else if (warnings.length > 0) {
|
|
1316
|
-
status = HEALTH_STATUS.DEGRADED;
|
|
1317
|
-
} else {
|
|
1318
|
-
status = HEALTH_STATUS.HEALTHY;
|
|
1319
|
-
}
|
|
1320
|
-
|
|
1321
|
-
const repairableCount = errors.filter(e => e.repairable).length +
|
|
1322
|
-
warnings.filter(w => w.repairable).length;
|
|
1323
|
-
|
|
1324
1353
|
// Check 11 (optional): drift analysis
|
|
1325
1354
|
let driftResult;
|
|
1326
1355
|
if (options.drift) {
|
|
@@ -1352,6 +1381,22 @@ function cmdValidateHealth(cwd, options, raw) {
|
|
|
1352
1381
|
}
|
|
1353
1382
|
}
|
|
1354
1383
|
|
|
1384
|
+
// Determine overall status from error/warning counts. This must run after every
|
|
1385
|
+
// check that can call addIssue -- computing it earlier left DRIFT_HIGH and
|
|
1386
|
+
// LINKS_ERR sitting in `warnings` while the verdict still read `healthy`, which
|
|
1387
|
+
// contradicts the documented behaviour of --links ("errors degrade health").
|
|
1388
|
+
let status;
|
|
1389
|
+
if (errors.length > 0) {
|
|
1390
|
+
status = HEALTH_STATUS.BROKEN;
|
|
1391
|
+
} else if (warnings.length > 0) {
|
|
1392
|
+
status = HEALTH_STATUS.DEGRADED;
|
|
1393
|
+
} else {
|
|
1394
|
+
status = HEALTH_STATUS.HEALTHY;
|
|
1395
|
+
}
|
|
1396
|
+
|
|
1397
|
+
const repairableCount = errors.filter(e => e.repairable).length +
|
|
1398
|
+
warnings.filter(w => w.repairable).length;
|
|
1399
|
+
|
|
1355
1400
|
const result = {
|
|
1356
1401
|
status,
|
|
1357
1402
|
errors,
|
|
@@ -79,7 +79,7 @@ function buildCounterfactualContext(cwd, phaseNum, scenario) {
|
|
|
79
79
|
|
|
80
80
|
return {
|
|
81
81
|
phase: String(phaseNum),
|
|
82
|
-
phase_name: phaseInfo.
|
|
82
|
+
phase_name: phaseInfo.phase_name || null,
|
|
83
83
|
directory: toPosix(phaseInfo.directory),
|
|
84
84
|
scenario,
|
|
85
85
|
slug: scenarioSlug(scenario),
|
|
@@ -256,6 +256,13 @@ function cmdWhatifPrepare(cwd, phaseNum, scenario, raw) {
|
|
|
256
256
|
const ctx = buildCounterfactualContext(cwd, phaseNum, scenario);
|
|
257
257
|
if (ctx.error) { output(ctx, raw); return; }
|
|
258
258
|
const wt = createWorktree(cwd, phaseNum, scenario);
|
|
259
|
+
// `worktree_error` (not `error`) because the payload also carries the context that
|
|
260
|
+
// WAS resolved, and `error` would read as "the context failed". It is still an
|
|
261
|
+
// error-family key, so output() derives exit 1 from it exactly as it does for the
|
|
262
|
+
// `ctx.error` guard one line above — which is the point: the two adjacent failures
|
|
263
|
+
// of the same command must not report different exit codes just because one of
|
|
264
|
+
// them renamed the key. Callers cannot proceed either way: what-if stage 2 spawns
|
|
265
|
+
// an agent INTO worktree_path, and there is no worktree.
|
|
259
266
|
if (wt.error) { output({ ...ctx, worktree_error: wt.error }, raw); return; }
|
|
260
267
|
output({ ...ctx, worktree: wt }, raw);
|
|
261
268
|
}
|
|
@@ -27,13 +27,13 @@
|
|
|
27
27
|
* config-get <key> Get a config.json value
|
|
28
28
|
* history-digest Aggregate all summary.md data
|
|
29
29
|
* summary-extract <path> [--fields] Extract structured data from summary.md
|
|
30
|
-
* state-snapshot Structured parse of state.md
|
|
30
|
+
* state-snapshot Structured parse of state.md (alias: state snapshot)
|
|
31
31
|
* phase-plan-index <phase> Index plans with waves and status
|
|
32
32
|
* websearch <query> Search web via Brave API (if configured)
|
|
33
33
|
* [--limit N] [--freshness day|week|month]
|
|
34
34
|
*
|
|
35
35
|
* Phase Listing:
|
|
36
|
-
* phases list [--type
|
|
36
|
+
* phases list [--type plans|summaries] List phases with optional type filter
|
|
37
37
|
* [--phase N] [--include-archived]
|
|
38
38
|
*
|
|
39
39
|
* Phase Operations:
|
|
@@ -282,8 +282,20 @@ async function main() {
|
|
|
282
282
|
|
|
283
283
|
const command = args[0];
|
|
284
284
|
|
|
285
|
+
const USAGE = 'Usage: pan-tools <command> [args] [--raw] [--cwd <path>]\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';
|
|
286
|
+
|
|
285
287
|
if (!command) {
|
|
286
|
-
error(
|
|
288
|
+
error(USAGE);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// P-1814 (PanLoop finding 11): `--help` was the single most frequent failed
|
|
292
|
+
// probe in field transcripts — 26 sightings across 10 command docs. It is
|
|
293
|
+
// the most predictable thing any agent or human tries, so it works: print
|
|
294
|
+
// the usage and exit 0. A missing command stays exit 1 (the error contract);
|
|
295
|
+
// an explicit request for help is a success.
|
|
296
|
+
if (command === '--help' || command === '-h' || command === 'help') {
|
|
297
|
+
console.log(USAGE);
|
|
298
|
+
process.exit(0);
|
|
287
299
|
}
|
|
288
300
|
|
|
289
301
|
switch (command) {
|
|
@@ -337,10 +349,16 @@ async function main() {
|
|
|
337
349
|
stopped_at: getArgValue(args, '--stopped-at'),
|
|
338
350
|
resume_file: getArgValue(args, '--resume-file', 'None'),
|
|
339
351
|
}, raw);
|
|
352
|
+
} else if (subcommand === 'snapshot') {
|
|
353
|
+
// Alias for the top-level `state-snapshot` (P-1813). The hyphenated
|
|
354
|
+
// command reads exactly like a `state` subcommand, and a field agent
|
|
355
|
+
// guessed the spaced form by analogy (PanLoop finding 10). The guess
|
|
356
|
+
// is semantically right, so it lands on the real handler.
|
|
357
|
+
state.cmdStateSnapshot(cwd, raw);
|
|
340
358
|
} else if (subcommand === 'load' || !subcommand) {
|
|
341
359
|
state.cmdStateLoad(cwd, raw);
|
|
342
360
|
} else {
|
|
343
|
-
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`);
|
|
361
|
+
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`);
|
|
344
362
|
}
|
|
345
363
|
break;
|
|
346
364
|
}
|
|
@@ -530,7 +548,7 @@ async function main() {
|
|
|
530
548
|
} else if (subcommand === 'stubs') {
|
|
531
549
|
verify.cmdVerifyStubs(cwd, { gate: args.includes('--gate') }, raw);
|
|
532
550
|
} else {
|
|
533
|
-
error('Unknown verify subcommand. Available: plan-structure, phase-completeness, references, commits, artifacts, key-links');
|
|
551
|
+
error('Unknown verify subcommand. Available: plan-structure, phase-completeness, references, commits, artifacts, key-links, reconcile, stubs');
|
|
534
552
|
}
|
|
535
553
|
break;
|
|
536
554
|
}
|
|
@@ -786,8 +804,12 @@ async function main() {
|
|
|
786
804
|
|
|
787
805
|
case 'batch-commit': {
|
|
788
806
|
const itemsJson = args[1];
|
|
789
|
-
|
|
790
|
-
|
|
807
|
+
// Report a malformed payload rather than silently reporting 'no_items':
|
|
808
|
+
// a caller whose JSON was garbled (e.g. shell-stripped quotes) must not be
|
|
809
|
+
// told there was nothing to commit. Absent arg → genuine empty batch.
|
|
810
|
+
const items = itemsJson === undefined || itemsJson === ''
|
|
811
|
+
? []
|
|
812
|
+
: parseJsonOrError(itemsJson, 'batch-commit');
|
|
791
813
|
commands.cmdBatchCommit(cwd, items, raw);
|
|
792
814
|
break;
|
|
793
815
|
}
|
|
@@ -1295,6 +1317,7 @@ async function main() {
|
|
|
1295
1317
|
const subcommand = args[1];
|
|
1296
1318
|
if (subcommand === 'trace') {
|
|
1297
1319
|
const traceSub = args[2];
|
|
1320
|
+
const tokensWasted = getArgValue(args, '--tokens-wasted');
|
|
1298
1321
|
optimize.cmdOptimizeTrace(cwd, traceSub, {
|
|
1299
1322
|
sessionId: getArgValue(args, '--session'),
|
|
1300
1323
|
all: args.includes('--all'),
|
|
@@ -1306,9 +1329,8 @@ async function main() {
|
|
|
1306
1329
|
type: getArgValue(args, '--type'),
|
|
1307
1330
|
category: getArgValue(args, '--category'),
|
|
1308
1331
|
impact: getArgValue(args, '--impact'),
|
|
1309
|
-
description: getArgValue(args, '--description'),
|
|
1310
1332
|
correction: getArgValue(args, '--correction'),
|
|
1311
|
-
tokens_wasted:
|
|
1333
|
+
tokens_wasted: tokensWasted ? Number(tokensWasted) : null,
|
|
1312
1334
|
context: (() => { const v = getArgValue(args, '--context'); if (!v) return null; try { return JSON.parse(v); } catch { return null; } })(),
|
|
1313
1335
|
}, raw);
|
|
1314
1336
|
} else if (subcommand === 'learn') {
|
|
@@ -1382,7 +1404,7 @@ async function main() {
|
|
|
1382
1404
|
const sourceExperiments = sourceExpsCsv
|
|
1383
1405
|
? sourceExpsCsv.split(',').map(s => s.trim()).filter(Boolean)
|
|
1384
1406
|
: [];
|
|
1385
|
-
const sourceRoot = getArgValue(args, '--source-root') ||
|
|
1407
|
+
const sourceRoot = getArgValue(args, '--source-root') || learnLint.resolveLearningsRoot();
|
|
1386
1408
|
|
|
1387
1409
|
const result = optimize.promotePattern(
|
|
1388
1410
|
{ id: patternId, summary, evidence, rule, applies_in: appliesIn, source_experiments: sourceExperiments },
|
|
@@ -1396,7 +1418,7 @@ async function main() {
|
|
|
1396
1418
|
const patternId = getArgValue(args, '--pattern');
|
|
1397
1419
|
const scope = getArgValue(args, '--scope');
|
|
1398
1420
|
const topic = getArgValue(args, '--topic');
|
|
1399
|
-
const sourceRoot = getArgValue(args, '--source-root') ||
|
|
1421
|
+
const sourceRoot = getArgValue(args, '--source-root') || learnLint.resolveLearningsRoot();
|
|
1400
1422
|
if (!patternId || !scope || !topic) {
|
|
1401
1423
|
error('learn unpromote requires --pattern <id> --scope <s> --topic <t>');
|
|
1402
1424
|
}
|
|
@@ -1406,14 +1428,14 @@ async function main() {
|
|
|
1406
1428
|
}
|
|
1407
1429
|
|
|
1408
1430
|
if (subcommand === 'list-promoted') {
|
|
1409
|
-
const sourceRoot = getArgValue(args, '--source-root') ||
|
|
1431
|
+
const sourceRoot = getArgValue(args, '--source-root') || learnLint.resolveLearningsRoot();
|
|
1410
1432
|
const result = optimize.listPromotedPatterns({ sourceRoot });
|
|
1411
1433
|
output(result, raw);
|
|
1412
1434
|
break;
|
|
1413
1435
|
}
|
|
1414
1436
|
|
|
1415
1437
|
if (subcommand === 'build-index') {
|
|
1416
|
-
const sourceRoot = getArgValue(args, '--source-root') ||
|
|
1438
|
+
const sourceRoot = getArgValue(args, '--source-root') || learnLint.resolveLearningsRoot();
|
|
1417
1439
|
const result = learnIndex.cmdBuildIndex(sourceRoot);
|
|
1418
1440
|
if (raw) {
|
|
1419
1441
|
output(result, true,
|
|
@@ -1428,7 +1450,7 @@ async function main() {
|
|
|
1428
1450
|
}
|
|
1429
1451
|
|
|
1430
1452
|
if (subcommand === 'topics-for') {
|
|
1431
|
-
const sourceRoot = getArgValue(args, '--source-root') ||
|
|
1453
|
+
const sourceRoot = getArgValue(args, '--source-root') || learnLint.resolveLearningsRoot();
|
|
1432
1454
|
const agent = getArgValue(args, '--agent');
|
|
1433
1455
|
if (!agent) { error('learn topics-for requires --agent <name>'); }
|
|
1434
1456
|
const minRelevance = getArgValue(args, '--min-relevance', 'medium');
|
|
@@ -1451,7 +1473,7 @@ async function main() {
|
|
|
1451
1473
|
}
|
|
1452
1474
|
|
|
1453
1475
|
if (subcommand === 'lint') {
|
|
1454
|
-
const sourceRoot = getArgValue(args, '--source-root') ||
|
|
1476
|
+
const sourceRoot = getArgValue(args, '--source-root') || learnLint.resolveLearningsRoot();
|
|
1455
1477
|
const scope = getArgValue(args, '--scope');
|
|
1456
1478
|
const strict = args.includes('--strict');
|
|
1457
1479
|
const result = learnLint.cmdLearnLint(sourceRoot, { scope, strict });
|
|
@@ -1466,11 +1488,12 @@ async function main() {
|
|
|
1466
1488
|
for (const v of result.violations) {
|
|
1467
1489
|
lines.push(`[${v.severity.toUpperCase()}] ${v.code} ${v.pattern_id}: ${v.message}`);
|
|
1468
1490
|
}
|
|
1469
|
-
output(result, true, lines.join('\n'));
|
|
1491
|
+
output(result, true, lines.join('\n'), result.summary.status === 'fail' ? 1 : 0);
|
|
1470
1492
|
} else {
|
|
1471
|
-
output(result, false);
|
|
1493
|
+
output(result, false, undefined, result.summary.status === 'fail' ? 1 : 0);
|
|
1472
1494
|
}
|
|
1473
|
-
|
|
1495
|
+
// exit code carried through output() above (it used to hard-exit 0
|
|
1496
|
+
// before this check, so `learn lint` never gated — M32, ADR audit 2026-08).
|
|
1474
1497
|
break;
|
|
1475
1498
|
}
|
|
1476
1499
|
|
|
@@ -1504,7 +1527,7 @@ async function main() {
|
|
|
1504
1527
|
}
|
|
1505
1528
|
|
|
1506
1529
|
default:
|
|
1507
|
-
error(`Unknown command: ${command}. Run pan-tools
|
|
1530
|
+
error(`Unknown command: ${command}. Run pan-tools --help to see available commands.`);
|
|
1508
1531
|
}
|
|
1509
1532
|
}
|
|
1510
1533
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schema_version": 1,
|
|
3
|
-
"generated_at": "2026-
|
|
3
|
+
"generated_at": "2026-08-07T13:36:47.532Z",
|
|
4
4
|
"topics": [
|
|
5
5
|
{
|
|
6
6
|
"name": "experiment-runner",
|
|
@@ -152,8 +152,8 @@
|
|
|
152
152
|
"P-350",
|
|
153
153
|
"P-360"
|
|
154
154
|
],
|
|
155
|
-
"size_bytes":
|
|
156
|
-
"size_tokens_est":
|
|
155
|
+
"size_bytes": 11837,
|
|
156
|
+
"size_tokens_est": 2960,
|
|
157
157
|
"agent_relevance": {
|
|
158
158
|
"planner": "low",
|
|
159
159
|
"executor": "medium",
|
|
@@ -218,8 +218,8 @@
|
|
|
218
218
|
"P-1204",
|
|
219
219
|
"P-NPRS-003"
|
|
220
220
|
],
|
|
221
|
-
"size_bytes":
|
|
222
|
-
"size_tokens_est":
|
|
221
|
+
"size_bytes": 3402,
|
|
222
|
+
"size_tokens_est": 851,
|
|
223
223
|
"agent_relevance": {
|
|
224
224
|
"planner": "medium",
|
|
225
225
|
"executor": "high",
|
|
@@ -804,7 +804,7 @@
|
|
|
804
804
|
"totals": {
|
|
805
805
|
"topics": 47,
|
|
806
806
|
"patterns": 93,
|
|
807
|
-
"size_bytes":
|
|
808
|
-
"size_tokens_est":
|
|
807
|
+
"size_bytes": 150126,
|
|
808
|
+
"size_tokens_est": 37549
|
|
809
809
|
}
|
|
810
810
|
}
|
|
@@ -37,12 +37,14 @@ patterns:
|
|
|
37
37
|
|
|
38
38
|
## P-330 — Scale agents into a coordinated army with squads, worktree isolation, and a human-gated ship
|
|
39
39
|
|
|
40
|
-
**Evidence:** The bot-army model (ADR-0032/0033) showed the durable shape for running a whole-project goal across many agents: a
|
|
40
|
+
**Evidence:** The bot-army model (ADR-0032/0033) showed the durable shape for running a whole-project goal across many agents: a coordinator that routes rather than implements fans work to role-scoped *squads* (architecture/build/quality/release), the build squad parallelizes by giving each agent its own branch + git worktree (which is why concurrent builders cannot land in the same working file), quality is adversarial and reads more than it writes, and the path to a protected branch is a human-approved gate, not a bot merge.
|
|
41
|
+
|
|
42
|
+
**Where the enforcement actually lives, and where it does not.** Worktree isolation is real isolation — separate directories, separate branches, enforced by git. The *role* labels are not: a coordinator that can spawn agents generally also holds file and shell tools, and a squad's `read-only` label is a contract its prompt is told to honour rather than a sandbox the runtime imposes. Both were written as absolutes here ("delegation-only", "never codes") and both were wrong; the guidance below survived the correction unchanged, because instructing the split is still right — claiming it is enforced is what was not.
|
|
41
43
|
|
|
42
44
|
**Rule:** When scaling beyond a single agent:
|
|
43
45
|
|
|
44
|
-
1. **
|
|
45
|
-
2. **Group agents into role-scoped squads with least-privilege
|
|
46
|
+
1. **Instruct the coordinator to delegate rather than implement** — it plans, decomposes, and routes to squads, then aggregates tight summaries. Treat that as prompt discipline and verify the grant instead of assuming it: read the coordinator's own `tools:` frontmatter, and if it holds write or shell tools, the split is instructed, not enforced. Say which of the two your prompt is relying on wherever you describe the design to a user.
|
|
47
|
+
2. **Group agents into role-scoped squads with a declared least-privilege contract** — design read-only, build read/write, quality read-only/adversarial, release always-ask. Resolve the roster from data, not hardcoded prompt lists. A label is a contract until a per-agent grant matches it; a registry that maps agents to role labels changes no execution path on its own, so publish those labels as advisory rather than as guarantees.
|
|
46
48
|
3. **Parallelize by isolation, not by hope** — one branch + worktree per concurrent builder; never two agents in one tree. Serialize builds only where the build tree corrupts under concurrency (a per-project opt-in).
|
|
47
49
|
4. **The mutating boundary is human-gated** — merging to a protected branch is `always-ask`; recovery is revert / previous tag, never force-push or history rewrite.
|
|
48
50
|
5. **The harness scales with the army, not after it** — depth caps, spawn/budget ceilings, and an abort kill-switch checked before every spawn are mandatory; a longer loop must not relax a single cap. Power and safety are the same investment.
|
|
@@ -18,7 +18,7 @@ patterns:
|
|
|
18
18
|
|
|
19
19
|
## P-1204 — O_EXCL lockfile + retry with bounded backoff is enough for multi-process file writes in Node — no flock needed
|
|
20
20
|
|
|
21
|
-
**Evidence:** whoocache lock.js + atomic-write.js: parallel-process tests with two child processes each calling set() 1000 times completed with consistent index, zero lost writes.
|
|
21
|
+
**Evidence:** whoocache lock.js + atomic-write.js: parallel-process tests with two child processes each calling set() 1000 times completed with consistent index, zero lost writes. The whoocache 02-02 summary records 'O_EXCL lockfile + Windows rename retry' shipped Phase 2.
|
|
22
22
|
|
|
23
23
|
**Rule:** For multi-process safe writes (parallel CLI invocations sharing one cache/index/state file), use fs.openSync(lockPath, 'wx') as a 'try-acquire' (EEXIST means held). On failure, retry with random backoff 5-50ms, capped at ~10 attempts. Always wrap acquired work in try/finally and unlink the lockfile in finally. Cross-platform safe (no flock dependency). Combine with the atomic write-tmp-then-rename pattern (P-1201) so even if the lock holder is killed mid-write, recovery is automatic.
|
|
24
24
|
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# Design Methodology (shared)
|
|
2
|
+
|
|
3
|
+
Single source of truth for how PAN designs a change **before** it is planned or
|
|
4
|
+
executed. Cited by `pan-designer` (main flow, via `/pan:design-phase`) and by
|
|
5
|
+
`focus-design` (the `/featureAI` feature pipeline). Both flows use the *same*
|
|
6
|
+
method at different **depth tiers** so design quality can't drift between them.
|
|
7
|
+
|
|
8
|
+
This reference defines: the depth tiers, what each tier must produce, and the
|
|
9
|
+
quality bar every design artifact is held to (the same bar `pan-design-checker`
|
|
10
|
+
verifies against). It does not prescribe a runtime — agents apply it with
|
|
11
|
+
whatever tools they have.
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## Depth tiers
|
|
16
|
+
|
|
17
|
+
A tier selects which sections of the design artifact (`templates/design.md`) are
|
|
18
|
+
**mandatory**. Deeper tiers are supersets of shallower ones. Pick the shallowest
|
|
19
|
+
tier that fits the altitude of the work.
|
|
20
|
+
|
|
21
|
+
| Tier | Altitude | Mandatory sections | Skips | Typical entry point |
|
|
22
|
+
|------|----------|--------------------|-------|---------------------|
|
|
23
|
+
| `spike` | Throwaway PoC / trivial change | Problem · Scope · Minimal architecture | Everything else — flag as speculative | `/pan:design-phase --spike`, `focus-design --spike` |
|
|
24
|
+
| `phase` | One roadmap phase | Problem · Success criteria (≥2 machine-checkable) · Architecture assessment · Design synthesis · Phase-scoped ADR · Threat-lite · Test hooks | Demand validation, competitive intel, market/strategy | **`/pan:design-phase` (main-flow default)** |
|
|
25
|
+
| `feature` | A whole feature (internal) | `phase` + Demand validation · Error-handling design · Full STRIDE-lite threat model · Test plan · Feature ladder | Competitive intel, strategic (Blue Ocean/Wardley) | `focus-design --internal` |
|
|
26
|
+
| `full` | A market-facing feature | `feature` + Competitive intelligence · Strategic analysis (ERRC) · Adoption analysis | — (nothing) | `focus-design --full` (DEFAULT) |
|
|
27
|
+
|
|
28
|
+
**Altitude rule (why the tiers exist):** product/strategic design — *whether*
|
|
29
|
+
and *what* to build, with demand and competitive evidence — is decided **once**
|
|
30
|
+
at feature/milestone creation (`focus-design`, or the product-design pass in
|
|
31
|
+
`milestone-new`). Per-phase design (`phase` tier) covers only *how* to build an
|
|
32
|
+
already-scoped phase: architecture, ADR, threat, synthesis. This keeps the
|
|
33
|
+
frequently-run per-phase path cheap and stops the main flow re-litigating product
|
|
34
|
+
questions on every phase.
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
## What every tier produces
|
|
39
|
+
|
|
40
|
+
The artifact is `{scope}-design.md` from `templates/design.md`. Sections below are
|
|
41
|
+
ordered; a tier makes a contiguous prefix (plus its named additions) mandatory.
|
|
42
|
+
|
|
43
|
+
### 1. Problem & scope (all tiers)
|
|
44
|
+
- **Problem statement** — what problem exists, why it matters, cost of inaction. Concrete, not a vague generality.
|
|
45
|
+
- **Scope** — in-scope / out-of-scope, and the fixed boundary this design must not exceed (a phase boundary from `roadmap.md`, or a feature boundary).
|
|
46
|
+
|
|
47
|
+
### 2. Success criteria (`phase`+)
|
|
48
|
+
- 3–7 criteria, **at least 2 machine-checkable** (verifiable by an automated test, not manual inspection). State the check for each machine-checkable one.
|
|
49
|
+
|
|
50
|
+
### 3. Architecture assessment & synthesis (`phase`+)
|
|
51
|
+
- **Conventions** — extract the project's actual conventions (module layout, error style, boundaries) from the codebase; do not assume.
|
|
52
|
+
- **Components & boundaries** — what modules are added/touched; confirm the design honors the discovered boundaries (no layer violations).
|
|
53
|
+
- **Interface contract** — exact invocation + input/output schema + status/exit codes, per project convention.
|
|
54
|
+
- **State & filesystem scope** — reads from / writes to (must stay within project root / `.planning/`), state mutations, side effects.
|
|
55
|
+
- **Design decisions** — each significant decision: what was decided, why, alternatives rejected.
|
|
56
|
+
|
|
57
|
+
### 4. ADR (`phase`+)
|
|
58
|
+
- A right-sized Architecture Decision Record: Status · Date · Context (problem, forces, current state, requirements traceability) · Decision (summary + numbered sub-decisions with rationale + integration) · Consequences (positive **and** negative, every negative with a mitigation or explicit acceptance).
|
|
59
|
+
- `phase` tier: phase-scoped ADR (may be a section in `design.md`). `feature`/`full`: a standalone `docs/decisions/ADR-NNNN-*.md`.
|
|
60
|
+
|
|
61
|
+
### 5. Threat model (`phase` = lite, `feature`+ = full)
|
|
62
|
+
- STRIDE-lite: for each realistic threat, its vector and its mitigation (or explicit acceptance). `phase` tier covers the change's own surface; `feature`+ covers the full feature.
|
|
63
|
+
|
|
64
|
+
### 6. Error handling & diagnostics (`feature`+)
|
|
65
|
+
- Every error condition → output shape + handling style (validate args before side effects; safe reads return null; actionable messages).
|
|
66
|
+
|
|
67
|
+
### 7. Test plan (`phase` = hooks, `feature`+ = full)
|
|
68
|
+
- How each success criterion is verified; which are unit vs scenario; the seed inputs for the machine-checkable ones.
|
|
69
|
+
|
|
70
|
+
### 8. Demand & strategy (`feature`+ / `full`)
|
|
71
|
+
- `feature`: demand evidence (or an explicit "speculative" flag), feature ladder (v0 MVP → v1 → v2).
|
|
72
|
+
- `full`: competitive intelligence, strategic analysis (ERRC: eliminate/reduce/raise/create), adoption analysis.
|
|
73
|
+
|
|
74
|
+
---
|
|
75
|
+
|
|
76
|
+
## Quality bar (what `pan-design-checker` enforces)
|
|
77
|
+
|
|
78
|
+
A design artifact **passes** only when all of these hold at its tier. This is the
|
|
79
|
+
same checklist the independent checker verifies — designers should self-check
|
|
80
|
+
against it before handing off, but passing is decided by the checker, not the
|
|
81
|
+
author.
|
|
82
|
+
|
|
83
|
+
1. **Requirement coverage** — every stated requirement maps to a design element; nothing is silently dropped.
|
|
84
|
+
2. **Machine-checkable criteria** — ≥2 success criteria are automatable, and each names its check (`phase`+).
|
|
85
|
+
3. **Architecture conformance** — the design matches conventions *discovered* from the codebase, and introduces no layer/boundary violation.
|
|
86
|
+
4. **ADR honesty** — alternatives are real (not strawmen); every negative consequence has a mitigation or is explicitly accepted; no placeholder/skeleton sections (`phase`+).
|
|
87
|
+
5. **Threat coverage** — each identified threat is mitigated or explicitly accepted; none is left open (`phase` lite / `feature`+ full).
|
|
88
|
+
6. **Testability** — the test plan/hooks map to the success criteria (`phase`+).
|
|
89
|
+
7. **Scope discipline** — the design stays within the fixed boundary; scope-expanding ideas are captured as "deferred", never designed in.
|
|
90
|
+
|
|
91
|
+
If any dimension fails, the checker returns structured gaps and the designer
|
|
92
|
+
revises. The reflexion loop is capped at **2 revision iterations** (design →
|
|
93
|
+
check → revise → check → final) to prevent over-revision — the same guardrail
|
|
94
|
+
`plan-phase` uses with `pan-plan-checker`.
|