pan-wizard 3.22.0 → 3.25.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 +4 -4
- 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 +34 -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/lib/worktree.cjs +98 -0
- package/pan-wizard-core/bin/pan-tools.cjs +45 -20
- 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
|
}
|
|
@@ -89,6 +89,91 @@ function listArmyWorktrees(cwd) {
|
|
|
89
89
|
return out;
|
|
90
90
|
}
|
|
91
91
|
|
|
92
|
+
/**
|
|
93
|
+
* Sweep every army worktree and orphaned army/ branch (P-1815, PanLoop
|
|
94
|
+
* finding 13). The what-if subsystem always had this (`whatif cleanup`); the
|
|
95
|
+
* army path created worktrees that nothing removed — sibling `pan-army-*`
|
|
96
|
+
* directories and `army/*` branches accumulated after every campaign.
|
|
97
|
+
*
|
|
98
|
+
* Safety posture (the L3 lesson — never trade clutter for silent data loss):
|
|
99
|
+
* - A DIRTY worktree is kept unless `force` — git refuses, we surface why.
|
|
100
|
+
* - A branch is deleted only when its tip is reachable from HEAD (truly
|
|
101
|
+
* integrated). Squash-merged and aborted branches are NOT reachable and may
|
|
102
|
+
* hold the only copy of real work, so by default they are KEPT and listed
|
|
103
|
+
* with the exact command to delete them; `force` sweeps them too.
|
|
104
|
+
* (The per-task Phase 5 teardown — `worktree remove --branch` right after
|
|
105
|
+
* the merge lands — deletes unconditionally; at that moment the deletion is
|
|
106
|
+
* the documented intent. The sweeper is the abort/orphan tool, so it errs
|
|
107
|
+
* the other way.)
|
|
108
|
+
* @param {string} cwd - main project root
|
|
109
|
+
* @param {Object} [opts] - { force: boolean }
|
|
110
|
+
* @returns {{removed_worktrees, deleted_branches, kept, pruned, clean}|{error}}
|
|
111
|
+
*/
|
|
112
|
+
function cleanupArmyWorktrees(cwd, opts) {
|
|
113
|
+
if (!isGitRepo(cwd)) return { error: 'Not a git repo' };
|
|
114
|
+
const force = opts?.force === true;
|
|
115
|
+
const removedWorktrees = [];
|
|
116
|
+
const deletedBranches = [];
|
|
117
|
+
const kept = [];
|
|
118
|
+
|
|
119
|
+
const branchIsIntegrated = (branch) =>
|
|
120
|
+
execGit(cwd, ['merge-base', '--is-ancestor', branch, 'HEAD']).exitCode === 0;
|
|
121
|
+
|
|
122
|
+
const deleteBranch = (branch, hadWorktree) => {
|
|
123
|
+
if (force || branchIsIntegrated(branch)) {
|
|
124
|
+
const del = execGit(cwd, ['branch', '-D', branch]);
|
|
125
|
+
if (del.exitCode === 0) deletedBranches.push(branch);
|
|
126
|
+
else kept.push({ branch, reason: `branch -D failed: ${del.stderr.trim()}` });
|
|
127
|
+
} else {
|
|
128
|
+
kept.push({
|
|
129
|
+
branch,
|
|
130
|
+
reason: `carries commits not reachable from HEAD (squash-merged or aborted work${hadWorktree ? '' : '; no worktree attached'}) — rerun with --force, or: git branch -D ${branch}`,
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
// 1. Registered army worktrees. Track every branch step 1 has already
|
|
136
|
+
// decided on — deleted OR deliberately kept — so the orphan scan below
|
|
137
|
+
// does not re-process (and double-report) it.
|
|
138
|
+
const handledBranches = new Set();
|
|
139
|
+
for (const t of listArmyWorktrees(cwd)) {
|
|
140
|
+
const rmArgs = ['worktree', 'remove'];
|
|
141
|
+
if (force) rmArgs.push('--force');
|
|
142
|
+
rmArgs.push(t.worktree);
|
|
143
|
+
const rm = execGit(cwd, rmArgs);
|
|
144
|
+
if (rm.exitCode !== 0) {
|
|
145
|
+
kept.push({ worktree: t.worktree, branch: t.branch, reason: `worktree remove refused: ${rm.stderr.trim()} — pass --force to discard uncommitted changes` });
|
|
146
|
+
if (t.branch) handledBranches.add(t.branch);
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
removedWorktrees.push(t.worktree);
|
|
150
|
+
if (t.branch) {
|
|
151
|
+
handledBranches.add(t.branch);
|
|
152
|
+
deleteBranch(t.branch, true);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// 2. Drop stale registrations (a manually deleted directory leaves one).
|
|
157
|
+
const pruned = execGit(cwd, ['worktree', 'prune']).exitCode === 0;
|
|
158
|
+
|
|
159
|
+
// 3. Orphaned army/ branches — a worktree removed without its branch.
|
|
160
|
+
const stillAttached = new Set(listArmyWorktrees(cwd).map(t => t.branch));
|
|
161
|
+
const ls = execGit(cwd, ['branch', '--list', `${ARMY_BRANCH_PREFIX}*`, '--format=%(refname:short)']);
|
|
162
|
+
if (ls.exitCode === 0) {
|
|
163
|
+
for (const branch of ls.stdout.split(/\r?\n/).map(s => s.trim()).filter(Boolean)) {
|
|
164
|
+
if (!stillAttached.has(branch) && !handledBranches.has(branch)) deleteBranch(branch, false);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return {
|
|
169
|
+
removed_worktrees: removedWorktrees,
|
|
170
|
+
deleted_branches: deletedBranches,
|
|
171
|
+
kept,
|
|
172
|
+
pruned,
|
|
173
|
+
clean: kept.length === 0,
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
|
|
92
177
|
// ─── CLI ─────────────────────────────────────────────────────────────────────
|
|
93
178
|
|
|
94
179
|
function cmdWorktreeList(cwd, raw) {
|
|
@@ -112,12 +197,25 @@ function cmdWorktreeRemove(cwd, worktreePath, branch, raw, opts) {
|
|
|
112
197
|
output(r, raw, r.warnings.length ? r.warnings.join('\n') : 'removed');
|
|
113
198
|
}
|
|
114
199
|
|
|
200
|
+
function cmdWorktreeCleanup(cwd, raw, opts) {
|
|
201
|
+
const r = cleanupArmyWorktrees(cwd, opts);
|
|
202
|
+
if (r.error) return error(r.error);
|
|
203
|
+
const lines = [
|
|
204
|
+
...r.removed_worktrees.map(w => `removed worktree ${w}`),
|
|
205
|
+
...r.deleted_branches.map(b => `deleted branch ${b}`),
|
|
206
|
+
...r.kept.map(k => `KEPT ${k.worktree || k.branch}: ${k.reason}`),
|
|
207
|
+
];
|
|
208
|
+
output(r, raw, lines.length ? lines.join('\n') : 'nothing to clean');
|
|
209
|
+
}
|
|
210
|
+
|
|
115
211
|
module.exports = {
|
|
116
212
|
ARMY_BRANCH_PREFIX,
|
|
117
213
|
createTaskWorktree,
|
|
118
214
|
removeTaskWorktree,
|
|
119
215
|
listArmyWorktrees,
|
|
216
|
+
cleanupArmyWorktrees,
|
|
120
217
|
cmdWorktreeList,
|
|
121
218
|
cmdWorktreeCreate,
|
|
122
219
|
cmdWorktreeRemove,
|
|
220
|
+
cmdWorktreeCleanup,
|
|
123
221
|
};
|
|
@@ -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
|
}
|
|
@@ -1169,8 +1191,10 @@ async function main() {
|
|
|
1169
1191
|
worktree.cmdWorktreeCreate(cwd, args[2], raw, { base: getArgValue(args, '--base') });
|
|
1170
1192
|
} else if (subcommand === 'remove') {
|
|
1171
1193
|
worktree.cmdWorktreeRemove(cwd, args[2], getArgValue(args, '--branch'), raw, { force: args.includes('--force') });
|
|
1194
|
+
} else if (subcommand === 'cleanup') {
|
|
1195
|
+
worktree.cmdWorktreeCleanup(cwd, raw, { force: args.includes('--force') });
|
|
1172
1196
|
} else {
|
|
1173
|
-
error('Unknown worktree subcommand. Available: list, create, remove');
|
|
1197
|
+
error('Unknown worktree subcommand. Available: list, create, remove, cleanup');
|
|
1174
1198
|
}
|
|
1175
1199
|
break;
|
|
1176
1200
|
}
|
|
@@ -1295,6 +1319,7 @@ async function main() {
|
|
|
1295
1319
|
const subcommand = args[1];
|
|
1296
1320
|
if (subcommand === 'trace') {
|
|
1297
1321
|
const traceSub = args[2];
|
|
1322
|
+
const tokensWasted = getArgValue(args, '--tokens-wasted');
|
|
1298
1323
|
optimize.cmdOptimizeTrace(cwd, traceSub, {
|
|
1299
1324
|
sessionId: getArgValue(args, '--session'),
|
|
1300
1325
|
all: args.includes('--all'),
|
|
@@ -1306,9 +1331,8 @@ async function main() {
|
|
|
1306
1331
|
type: getArgValue(args, '--type'),
|
|
1307
1332
|
category: getArgValue(args, '--category'),
|
|
1308
1333
|
impact: getArgValue(args, '--impact'),
|
|
1309
|
-
description: getArgValue(args, '--description'),
|
|
1310
1334
|
correction: getArgValue(args, '--correction'),
|
|
1311
|
-
tokens_wasted:
|
|
1335
|
+
tokens_wasted: tokensWasted ? Number(tokensWasted) : null,
|
|
1312
1336
|
context: (() => { const v = getArgValue(args, '--context'); if (!v) return null; try { return JSON.parse(v); } catch { return null; } })(),
|
|
1313
1337
|
}, raw);
|
|
1314
1338
|
} else if (subcommand === 'learn') {
|
|
@@ -1382,7 +1406,7 @@ async function main() {
|
|
|
1382
1406
|
const sourceExperiments = sourceExpsCsv
|
|
1383
1407
|
? sourceExpsCsv.split(',').map(s => s.trim()).filter(Boolean)
|
|
1384
1408
|
: [];
|
|
1385
|
-
const sourceRoot = getArgValue(args, '--source-root') ||
|
|
1409
|
+
const sourceRoot = getArgValue(args, '--source-root') || learnLint.resolveLearningsRoot();
|
|
1386
1410
|
|
|
1387
1411
|
const result = optimize.promotePattern(
|
|
1388
1412
|
{ id: patternId, summary, evidence, rule, applies_in: appliesIn, source_experiments: sourceExperiments },
|
|
@@ -1396,7 +1420,7 @@ async function main() {
|
|
|
1396
1420
|
const patternId = getArgValue(args, '--pattern');
|
|
1397
1421
|
const scope = getArgValue(args, '--scope');
|
|
1398
1422
|
const topic = getArgValue(args, '--topic');
|
|
1399
|
-
const sourceRoot = getArgValue(args, '--source-root') ||
|
|
1423
|
+
const sourceRoot = getArgValue(args, '--source-root') || learnLint.resolveLearningsRoot();
|
|
1400
1424
|
if (!patternId || !scope || !topic) {
|
|
1401
1425
|
error('learn unpromote requires --pattern <id> --scope <s> --topic <t>');
|
|
1402
1426
|
}
|
|
@@ -1406,14 +1430,14 @@ async function main() {
|
|
|
1406
1430
|
}
|
|
1407
1431
|
|
|
1408
1432
|
if (subcommand === 'list-promoted') {
|
|
1409
|
-
const sourceRoot = getArgValue(args, '--source-root') ||
|
|
1433
|
+
const sourceRoot = getArgValue(args, '--source-root') || learnLint.resolveLearningsRoot();
|
|
1410
1434
|
const result = optimize.listPromotedPatterns({ sourceRoot });
|
|
1411
1435
|
output(result, raw);
|
|
1412
1436
|
break;
|
|
1413
1437
|
}
|
|
1414
1438
|
|
|
1415
1439
|
if (subcommand === 'build-index') {
|
|
1416
|
-
const sourceRoot = getArgValue(args, '--source-root') ||
|
|
1440
|
+
const sourceRoot = getArgValue(args, '--source-root') || learnLint.resolveLearningsRoot();
|
|
1417
1441
|
const result = learnIndex.cmdBuildIndex(sourceRoot);
|
|
1418
1442
|
if (raw) {
|
|
1419
1443
|
output(result, true,
|
|
@@ -1428,7 +1452,7 @@ async function main() {
|
|
|
1428
1452
|
}
|
|
1429
1453
|
|
|
1430
1454
|
if (subcommand === 'topics-for') {
|
|
1431
|
-
const sourceRoot = getArgValue(args, '--source-root') ||
|
|
1455
|
+
const sourceRoot = getArgValue(args, '--source-root') || learnLint.resolveLearningsRoot();
|
|
1432
1456
|
const agent = getArgValue(args, '--agent');
|
|
1433
1457
|
if (!agent) { error('learn topics-for requires --agent <name>'); }
|
|
1434
1458
|
const minRelevance = getArgValue(args, '--min-relevance', 'medium');
|
|
@@ -1451,7 +1475,7 @@ async function main() {
|
|
|
1451
1475
|
}
|
|
1452
1476
|
|
|
1453
1477
|
if (subcommand === 'lint') {
|
|
1454
|
-
const sourceRoot = getArgValue(args, '--source-root') ||
|
|
1478
|
+
const sourceRoot = getArgValue(args, '--source-root') || learnLint.resolveLearningsRoot();
|
|
1455
1479
|
const scope = getArgValue(args, '--scope');
|
|
1456
1480
|
const strict = args.includes('--strict');
|
|
1457
1481
|
const result = learnLint.cmdLearnLint(sourceRoot, { scope, strict });
|
|
@@ -1466,11 +1490,12 @@ async function main() {
|
|
|
1466
1490
|
for (const v of result.violations) {
|
|
1467
1491
|
lines.push(`[${v.severity.toUpperCase()}] ${v.code} ${v.pattern_id}: ${v.message}`);
|
|
1468
1492
|
}
|
|
1469
|
-
output(result, true, lines.join('\n'));
|
|
1493
|
+
output(result, true, lines.join('\n'), result.summary.status === 'fail' ? 1 : 0);
|
|
1470
1494
|
} else {
|
|
1471
|
-
output(result, false);
|
|
1495
|
+
output(result, false, undefined, result.summary.status === 'fail' ? 1 : 0);
|
|
1472
1496
|
}
|
|
1473
|
-
|
|
1497
|
+
// exit code carried through output() above (it used to hard-exit 0
|
|
1498
|
+
// before this check, so `learn lint` never gated — M32, ADR audit 2026-08).
|
|
1474
1499
|
break;
|
|
1475
1500
|
}
|
|
1476
1501
|
|
|
@@ -1504,7 +1529,7 @@ async function main() {
|
|
|
1504
1529
|
}
|
|
1505
1530
|
|
|
1506
1531
|
default:
|
|
1507
|
-
error(`Unknown command: ${command}. Run pan-tools
|
|
1532
|
+
error(`Unknown command: ${command}. Run pan-tools --help to see available commands.`);
|
|
1508
1533
|
}
|
|
1509
1534
|
}
|
|
1510
1535
|
|
|
@@ -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.
|