pan-wizard 3.8.0 → 3.12.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 +80 -9
- package/agents/pan-conductor.md +15 -3
- package/agents/pan-counterfactual.md +1 -2
- package/agents/pan-debugger.md +1 -2
- package/agents/pan-distiller.md +1 -2
- package/agents/pan-document_code.md +1 -0
- package/agents/pan-executor.md +1 -0
- package/agents/pan-experiment-runner.md +1 -2
- package/agents/pan-hardener.md +1 -2
- package/agents/pan-integration-checker.md +1 -2
- package/agents/pan-knowledge.md +1 -2
- package/agents/pan-meta-reviewer.md +1 -2
- package/agents/pan-optimizer.md +1 -0
- package/agents/pan-phase-researcher.md +1 -0
- package/agents/pan-plan-checker.md +1 -2
- package/agents/pan-planner.md +1 -0
- package/agents/pan-previewer.md +1 -2
- package/agents/pan-project-researcher.md +6 -0
- package/agents/pan-release.md +58 -0
- package/agents/pan-research-synthesizer.md +7 -0
- package/agents/pan-reviewer.md +2 -3
- package/agents/pan-roadmapper.md +1 -0
- package/agents/pan-verifier.md +1 -2
- package/assets/pan-avatar.png +0 -0
- package/assets/pan-developer.png +0 -0
- package/assets/pan-docs-header.png +0 -0
- package/assets/pan-hero.png +0 -0
- package/assets/pan-logo-2000-transparent.svg +11 -30
- package/assets/pan-logo-2000.svg +12 -43
- package/assets/pan-logo-lockup.svg +11 -0
- package/assets/pan-mark.svg +7 -0
- package/assets/pan-orchestration.png +0 -0
- package/assets/pan-readme-hero.png +0 -0
- package/assets/terminal.svg +39 -119
- package/bin/install-lib.cjs +661 -46
- package/bin/install.js +722 -116
- package/commands/pan/army.md +169 -0
- package/commands/pan/dashboard.md +25 -0
- package/commands/pan/experiment.md +2 -0
- package/commands/pan/focus-auto.md +32 -4
- package/commands/pan/hud.md +91 -0
- package/commands/pan/profile.md +2 -0
- package/hooks/dist/pan-cost-logger.js +22 -7
- package/package.json +5 -4
- package/pan-wizard-core/bin/lib/campaign.cjs +198 -0
- package/pan-wizard-core/bin/lib/commands-learnings.cjs +544 -0
- package/pan-wizard-core/bin/lib/commands.cjs +12 -523
- package/pan-wizard-core/bin/lib/constants.cjs +8 -0
- package/pan-wizard-core/bin/lib/core.cjs +80 -0
- package/pan-wizard-core/bin/lib/cost.cjs +62 -8
- package/pan-wizard-core/bin/lib/focus.cjs +13 -1
- package/pan-wizard-core/bin/lib/git.cjs +6 -1
- package/pan-wizard-core/bin/lib/hud.cjs +887 -0
- package/pan-wizard-core/bin/lib/lock.cjs +108 -0
- package/pan-wizard-core/bin/lib/milestone.cjs +3 -2
- package/pan-wizard-core/bin/lib/phase-remove.cjs +392 -0
- package/pan-wizard-core/bin/lib/phase.cjs +4 -369
- package/pan-wizard-core/bin/lib/runner.cjs +5 -0
- package/pan-wizard-core/bin/lib/squads.cjs +152 -0
- package/pan-wizard-core/bin/lib/state.cjs +10 -1
- package/pan-wizard-core/bin/lib/verify-deploy.cjs +181 -0
- package/pan-wizard-core/bin/lib/verify-drift.cjs +255 -0
- package/pan-wizard-core/bin/lib/verify-preflight.cjs +261 -0
- package/pan-wizard-core/bin/lib/verify-retro.cjs +177 -0
- package/pan-wizard-core/bin/lib/verify.cjs +10 -797
- package/pan-wizard-core/bin/lib/worktree.cjs +123 -0
- package/pan-wizard-core/bin/pan-tools.cjs +78 -0
- package/pan-wizard-core/learnings/universal/autonomous-loop.md +56 -0
- package/pan-wizard-core/workflows/plan-phase.md +11 -0
- package/scripts/build-plugin.js +105 -0
- package/scripts/install-git-hooks.js +64 -0
- package/scripts/release-check.js +13 -2
|
@@ -10,6 +10,9 @@ const { writeStateMd, readStateSafe } = require('./state.cjs');
|
|
|
10
10
|
const { enumerateRoadmapPhases } = require('./roadmap.cjs');
|
|
11
11
|
const { PLANNING_DIR, PHASES_DIR, ROADMAP_FILE, REQUIREMENTS_FILE, STATE_FILE, isPlanFile, isSummaryFile, getPlanId, PHASE_DIR_RE, ARCHIVE_DIR_RE } = require('./constants.cjs');
|
|
12
12
|
const { planningPath, phasesPath, filterPlanFiles, filterSummaryFiles, parsePhaseDir, fileAccessible } = require('./utils.cjs');
|
|
13
|
+
// Phase removal lives in phase-remove.cjs; re-exported below so consumers of
|
|
14
|
+
// phase.cjs are unaffected by the decomposition.
|
|
15
|
+
const { removePhaseFromDisk, renumberDecimalPhases, renumberIntegerPhases, updateRoadmapAfterRemoval, cmdPhaseRemove } = require('./phase-remove.cjs');
|
|
13
16
|
|
|
14
17
|
/**
|
|
15
18
|
* List phase directories or files within phases, with optional type and archive filtering.
|
|
@@ -516,375 +519,7 @@ function cmdPhaseInsert(cwd, afterPhase, description, raw) {
|
|
|
516
519
|
output(result, raw, decimalPhase);
|
|
517
520
|
}
|
|
518
521
|
|
|
519
|
-
// ───
|
|
520
|
-
|
|
521
|
-
/**
|
|
522
|
-
* Delete a phase directory from disk.
|
|
523
|
-
* @param {string} phaseDir - Absolute path to the phase directory to remove
|
|
524
|
-
*/
|
|
525
|
-
function removePhaseFromDisk(phaseDir) {
|
|
526
|
-
try {
|
|
527
|
-
fs.rmSync(phaseDir, { recursive: true, force: true });
|
|
528
|
-
} catch (e) {
|
|
529
|
-
return { removed: false, error: e.message };
|
|
530
|
-
}
|
|
531
|
-
return { removed: true };
|
|
532
|
-
}
|
|
533
|
-
|
|
534
|
-
/**
|
|
535
|
-
* Renumber sibling decimal phases after one is removed.
|
|
536
|
-
*
|
|
537
|
-
* Algorithm: When a decimal phase like 06.2 is removed, all higher-numbered
|
|
538
|
-
* siblings under the same base integer (06.3, 06.4, ...) must be decremented
|
|
539
|
-
* by 1 to fill the gap. We process in descending order to avoid directory
|
|
540
|
-
* name collisions during rename (e.g. rename 06.4 -> 06.3 before 06.3 -> 06.2).
|
|
541
|
-
*
|
|
542
|
-
* @param {string} phasesDir - Absolute path to the phases directory
|
|
543
|
-
* @param {string} baseInt - The integer portion of the removed phase (e.g. "06")
|
|
544
|
-
* @param {number} removedDecimal - The decimal portion that was removed (e.g. 2)
|
|
545
|
-
* @returns {{ renamedDirs: Array, renamedFiles: Array }}
|
|
546
|
-
*/
|
|
547
|
-
function renumberDecimalPhases(phasesDir, baseInt, removedDecimal) {
|
|
548
|
-
const renamedDirs = [];
|
|
549
|
-
const renamedFiles = [];
|
|
550
|
-
|
|
551
|
-
try {
|
|
552
|
-
const entries = fs.readdirSync(phasesDir, { withFileTypes: true });
|
|
553
|
-
const dirs = entries.filter(entry => entry.isDirectory()).map(entry => entry.name).sort((left, right) => comparePhaseNum(left, right));
|
|
554
|
-
|
|
555
|
-
// Find sibling decimals with higher numbers than the removed one
|
|
556
|
-
const decPattern = new RegExp(`^${baseInt}\\.(\\d+)-(.+)$`);
|
|
557
|
-
const toRename = [];
|
|
558
|
-
for (const dir of dirs) {
|
|
559
|
-
const decMatch = dir.match(decPattern);
|
|
560
|
-
if (decMatch && parseInt(decMatch[1], 10) > removedDecimal) {
|
|
561
|
-
toRename.push({ dir, oldDecimal: parseInt(decMatch[1], 10), slug: decMatch[2] });
|
|
562
|
-
}
|
|
563
|
-
}
|
|
564
|
-
|
|
565
|
-
// Sort descending so higher-numbered dirs are renamed first,
|
|
566
|
-
// preventing collisions (e.g. 06.4 -> 06.3 before 06.3 -> 06.2)
|
|
567
|
-
toRename.sort((left, right) => right.oldDecimal - left.oldDecimal);
|
|
568
|
-
|
|
569
|
-
for (const item of toRename) {
|
|
570
|
-
const newDecimal = item.oldDecimal - 1;
|
|
571
|
-
const oldPhaseId = `${baseInt}.${item.oldDecimal}`;
|
|
572
|
-
const newPhaseId = `${baseInt}.${newDecimal}`;
|
|
573
|
-
const newDirName = `${baseInt}.${newDecimal}-${item.slug}`;
|
|
574
|
-
|
|
575
|
-
// Rename the directory itself (e.g. 06.3-foo -> 06.2-foo)
|
|
576
|
-
fs.renameSync(path.join(phasesDir, item.dir), path.join(phasesDir, newDirName));
|
|
577
|
-
renamedDirs.push({ from: item.dir, to: newDirName });
|
|
578
|
-
|
|
579
|
-
// Rename files inside that contain the old phase ID prefix
|
|
580
|
-
const dirFiles = fs.readdirSync(path.join(phasesDir, newDirName));
|
|
581
|
-
for (const file of dirFiles) {
|
|
582
|
-
// Files may have phase prefix like "06.2-01-plan.md"
|
|
583
|
-
if (file.includes(oldPhaseId)) {
|
|
584
|
-
const newFileName = file.replace(oldPhaseId, newPhaseId);
|
|
585
|
-
fs.renameSync(
|
|
586
|
-
path.join(phasesDir, newDirName, file),
|
|
587
|
-
path.join(phasesDir, newDirName, newFileName)
|
|
588
|
-
);
|
|
589
|
-
renamedFiles.push({ from: file, to: newFileName });
|
|
590
|
-
}
|
|
591
|
-
}
|
|
592
|
-
}
|
|
593
|
-
} catch (e) {
|
|
594
|
-
return { renamedDirs, renamedFiles, error: `Partial rename: ${e.message}` };
|
|
595
|
-
}
|
|
596
|
-
|
|
597
|
-
return { renamedDirs, renamedFiles };
|
|
598
|
-
}
|
|
599
|
-
|
|
600
|
-
/**
|
|
601
|
-
* Renumber integer phases (and their decimal/letter children) after one is removed.
|
|
602
|
-
*
|
|
603
|
-
* Algorithm: When an integer phase like 05 is removed, all phases with a higher
|
|
604
|
-
* integer base (06, 06.1, 06A, 07, ...) must be decremented by 1. We process
|
|
605
|
-
* in descending order to avoid directory name collisions during the rename
|
|
606
|
-
* cascade (e.g. rename 07 -> 06 before 06 -> 05). Each directory and its
|
|
607
|
-
* contained files with the old phase prefix are renamed to reflect the new number.
|
|
608
|
-
*
|
|
609
|
-
* @param {string} phasesDir - Absolute path to the phases directory
|
|
610
|
-
* @param {number} removedInt - The integer phase number that was removed
|
|
611
|
-
* @returns {{ renamedDirs: Array, renamedFiles: Array }}
|
|
612
|
-
*/
|
|
613
|
-
/**
|
|
614
|
-
* Collect phase directories that need renumbering (integer > removedInt).
|
|
615
|
-
* @param {string[]} dirs - Sorted directory names
|
|
616
|
-
* @param {number} removedInt - Removed phase integer
|
|
617
|
-
* @returns {Array} Items to rename, sorted descending to avoid collisions
|
|
618
|
-
*/
|
|
619
|
-
function collectDirsToRenumber(dirs, removedInt) {
|
|
620
|
-
const toRename = [];
|
|
621
|
-
for (const dir of dirs) {
|
|
622
|
-
const dirMatch = dir.match(/^(\d+)([A-Z])?(?:\.(\d+))?-(.+)$/i);
|
|
623
|
-
if (!dirMatch) continue;
|
|
624
|
-
const dirInt = parseInt(dirMatch[1], 10);
|
|
625
|
-
if (dirInt > removedInt) {
|
|
626
|
-
toRename.push({
|
|
627
|
-
dir, oldInt: dirInt,
|
|
628
|
-
letter: dirMatch[2] ? dirMatch[2].toUpperCase() : '',
|
|
629
|
-
decimal: dirMatch[3] ? parseInt(dirMatch[3], 10) : null,
|
|
630
|
-
slug: dirMatch[4],
|
|
631
|
-
});
|
|
632
|
-
}
|
|
633
|
-
}
|
|
634
|
-
toRename.sort((left, right) => {
|
|
635
|
-
if (left.oldInt !== right.oldInt) return right.oldInt - left.oldInt;
|
|
636
|
-
return (right.decimal || 0) - (left.decimal || 0);
|
|
637
|
-
});
|
|
638
|
-
return toRename;
|
|
639
|
-
}
|
|
640
|
-
|
|
641
|
-
/**
|
|
642
|
-
* Rename a single phase directory and its internal files.
|
|
643
|
-
* @param {string} phasesDir - Phases directory path
|
|
644
|
-
* @param {Object} item - Rename item from collectDirsToRenumber
|
|
645
|
-
* @param {Array} renamedDirs - Accumulator for renamed dirs
|
|
646
|
-
* @param {Array} renamedFiles - Accumulator for renamed files
|
|
647
|
-
*/
|
|
648
|
-
function renamePhaseDir(phasesDir, item, renamedDirs, renamedFiles) {
|
|
649
|
-
const newPadded = String(item.oldInt - 1).padStart(2, '0');
|
|
650
|
-
const oldPadded = String(item.oldInt).padStart(2, '0');
|
|
651
|
-
const suffix = (item.letter || '') + (item.decimal !== null ? `.${item.decimal}` : '');
|
|
652
|
-
const oldPrefix = oldPadded + suffix;
|
|
653
|
-
const newPrefix = newPadded + suffix;
|
|
654
|
-
const newDirName = `${newPrefix}-${item.slug}`;
|
|
655
|
-
|
|
656
|
-
fs.renameSync(path.join(phasesDir, item.dir), path.join(phasesDir, newDirName));
|
|
657
|
-
renamedDirs.push({ from: item.dir, to: newDirName });
|
|
658
|
-
|
|
659
|
-
for (const file of fs.readdirSync(path.join(phasesDir, newDirName))) {
|
|
660
|
-
if (file.startsWith(oldPrefix)) {
|
|
661
|
-
const newFileName = newPrefix + file.slice(oldPrefix.length);
|
|
662
|
-
fs.renameSync(path.join(phasesDir, newDirName, file), path.join(phasesDir, newDirName, newFileName));
|
|
663
|
-
renamedFiles.push({ from: file, to: newFileName });
|
|
664
|
-
}
|
|
665
|
-
}
|
|
666
|
-
}
|
|
667
|
-
|
|
668
|
-
function renumberIntegerPhases(phasesDir, removedInt) {
|
|
669
|
-
const renamedDirs = [];
|
|
670
|
-
const renamedFiles = [];
|
|
671
|
-
try {
|
|
672
|
-
const entries = fs.readdirSync(phasesDir, { withFileTypes: true });
|
|
673
|
-
const dirs = entries.filter(e => e.isDirectory()).map(e => e.name).sort((a, b) => comparePhaseNum(a, b));
|
|
674
|
-
const toRename = collectDirsToRenumber(dirs, removedInt);
|
|
675
|
-
for (const item of toRename) renamePhaseDir(phasesDir, item, renamedDirs, renamedFiles);
|
|
676
|
-
} catch (e) {
|
|
677
|
-
return { renamedDirs, renamedFiles, error: `Partial rename: ${e.message}` };
|
|
678
|
-
}
|
|
679
|
-
return { renamedDirs, renamedFiles };
|
|
680
|
-
}
|
|
681
|
-
|
|
682
|
-
/**
|
|
683
|
-
* Rewrite roadmap.md after a phase is removed: delete the target section,
|
|
684
|
-
* remove checkbox/table references, and renumber subsequent phase references.
|
|
685
|
-
*
|
|
686
|
-
* @param {string} cwd - Working directory path
|
|
687
|
-
* @param {string} phaseNum - The phase number that was removed (as originally specified)
|
|
688
|
-
* @param {boolean} isDecimal - Whether the removed phase was a decimal phase
|
|
689
|
-
* @param {string} normalized - The zero-padded normalized phase number
|
|
690
|
-
*/
|
|
691
|
-
function updateRoadmapAfterRemoval(cwd, phaseNum, isDecimal, normalized) {
|
|
692
|
-
const roadmapPath = path.join(planningPath(cwd), ROADMAP_FILE);
|
|
693
|
-
let roadmapContent;
|
|
694
|
-
try {
|
|
695
|
-
roadmapContent = fs.readFileSync(roadmapPath, 'utf-8');
|
|
696
|
-
} catch { return; }
|
|
697
|
-
|
|
698
|
-
// Remove the target phase section from roadmap.md.
|
|
699
|
-
// Matches from the phase heading to the next phase heading (or end of file).
|
|
700
|
-
const targetEscaped = escapeRegex(phaseNum);
|
|
701
|
-
const sectionPattern = new RegExp(
|
|
702
|
-
`\\n?#{2,4}\\s*Phase\\s+${targetEscaped}\\s*:[\\s\\S]*?(?=\\n#{2,4}\\s+Phase\\s+\\d|$)`,
|
|
703
|
-
'i'
|
|
704
|
-
);
|
|
705
|
-
roadmapContent = roadmapContent.replace(sectionPattern, '');
|
|
706
|
-
|
|
707
|
-
// Remove checkbox list items referencing this phase
|
|
708
|
-
const checkboxPattern = new RegExp(`\\n?-\\s*\\[[ x]\\]\\s*.*Phase\\s+${targetEscaped}[:\\s][^\\n]*`, 'gi');
|
|
709
|
-
roadmapContent = roadmapContent.replace(checkboxPattern, '');
|
|
710
|
-
|
|
711
|
-
// Remove progress table rows referencing this phase
|
|
712
|
-
const tableRowPattern = new RegExp(`\\n?\\|\\s*${targetEscaped}\\.?\\s[^|]*\\|[^\\n]*`, 'gi');
|
|
713
|
-
roadmapContent = roadmapContent.replace(tableRowPattern, '');
|
|
714
|
-
|
|
715
|
-
// For integer phase removal, renumber all references to subsequent phases.
|
|
716
|
-
// Walk from highest phase number down to removedInt+1, decrementing each by 1.
|
|
717
|
-
// This avoids double-renaming (e.g. 8->7 then 7->6 would break if done ascending).
|
|
718
|
-
if (!isDecimal) {
|
|
719
|
-
const removedInt = parseInt(normalized, 10);
|
|
720
|
-
|
|
721
|
-
// Reasonable upper bound for phase numbers
|
|
722
|
-
const maxPhase = 99;
|
|
723
|
-
for (let oldNum = maxPhase; oldNum > removedInt; oldNum--) {
|
|
724
|
-
const newNum = oldNum - 1;
|
|
725
|
-
const oldStr = String(oldNum);
|
|
726
|
-
const newStr = String(newNum);
|
|
727
|
-
const oldPad = oldStr.padStart(2, '0');
|
|
728
|
-
const newPad = newStr.padStart(2, '0');
|
|
729
|
-
|
|
730
|
-
// Phase headings: ## Phase 18: or ### Phase 18: -> ## Phase 17:
|
|
731
|
-
roadmapContent = roadmapContent.replace(
|
|
732
|
-
new RegExp(`(#{2,4}\\s*Phase\\s+)${oldStr}(\\s*:)`, 'gi'),
|
|
733
|
-
`$1${newStr}$2`
|
|
734
|
-
);
|
|
735
|
-
|
|
736
|
-
// Inline phase references: "Phase 18:" or "Phase 18 " -> "Phase 17:"
|
|
737
|
-
roadmapContent = roadmapContent.replace(
|
|
738
|
-
new RegExp(`(Phase\\s+)${oldStr}([:\\s])`, 'g'),
|
|
739
|
-
`$1${newStr}$2`
|
|
740
|
-
);
|
|
741
|
-
|
|
742
|
-
// Plan references in padded form: 18-01 -> 17-01
|
|
743
|
-
roadmapContent = roadmapContent.replace(
|
|
744
|
-
new RegExp(`${oldPad}-(\\d{2})`, 'g'),
|
|
745
|
-
`${newPad}-$1`
|
|
746
|
-
);
|
|
747
|
-
|
|
748
|
-
// Progress table row numbers: | 18. -> | 17.
|
|
749
|
-
roadmapContent = roadmapContent.replace(
|
|
750
|
-
new RegExp(`(\\|\\s*)${oldStr}\\.\\s`, 'g'),
|
|
751
|
-
`$1${newStr}. `
|
|
752
|
-
);
|
|
753
|
-
|
|
754
|
-
// Depends-on references: "Depends on:** Phase 18" -> "Phase 17"
|
|
755
|
-
roadmapContent = roadmapContent.replace(
|
|
756
|
-
new RegExp(`(Depends on:\\*\\*\\s*Phase\\s+)${oldStr}\\b`, 'gi'),
|
|
757
|
-
`$1${newStr}`
|
|
758
|
-
);
|
|
759
|
-
}
|
|
760
|
-
}
|
|
761
|
-
|
|
762
|
-
try { fs.writeFileSync(roadmapPath, roadmapContent, 'utf-8'); } catch { /* best-effort */ }
|
|
763
|
-
}
|
|
764
|
-
|
|
765
|
-
/**
|
|
766
|
-
* Remove a phase directory, renumber subsequent phases, and update ROADMAP/STATE.
|
|
767
|
-
* @param {string} cwd - Working directory path
|
|
768
|
-
* @param {string} targetPhase - Phase number to remove
|
|
769
|
-
* @param {Object} options - Options (force: skip executed-work check)
|
|
770
|
-
* @param {boolean} raw - If true, output raw value instead of JSON
|
|
771
|
-
* @returns {void}
|
|
772
|
-
*/
|
|
773
|
-
function cmdPhaseRemove(cwd, targetPhase, options, raw) {
|
|
774
|
-
if (!targetPhase) {
|
|
775
|
-
error('phase number required for phase remove');
|
|
776
|
-
}
|
|
777
|
-
|
|
778
|
-
const roadmapPath = path.join(planningPath(cwd), ROADMAP_FILE);
|
|
779
|
-
const phasesDir = phasesPath(cwd);
|
|
780
|
-
const force = options.force || false;
|
|
781
|
-
|
|
782
|
-
if (!fileAccessible(roadmapPath)) {
|
|
783
|
-
error('roadmap.md not found');
|
|
784
|
-
}
|
|
785
|
-
|
|
786
|
-
// Normalize the target
|
|
787
|
-
const normalized = normalizePhaseName(targetPhase);
|
|
788
|
-
const isDecimal = targetPhase.includes('.');
|
|
789
|
-
|
|
790
|
-
// Find and validate target directory
|
|
791
|
-
let targetDir = null;
|
|
792
|
-
try {
|
|
793
|
-
const entries = fs.readdirSync(phasesDir, { withFileTypes: true });
|
|
794
|
-
const dirs = entries.filter(entry => entry.isDirectory()).map(entry => entry.name).sort((left, right) => comparePhaseNum(left, right));
|
|
795
|
-
targetDir = dirs.find(dir => dir.startsWith(normalized + '-') || dir === normalized);
|
|
796
|
-
} catch {
|
|
797
|
-
// Phases directory does not exist; targetDir remains null
|
|
798
|
-
}
|
|
799
|
-
|
|
800
|
-
// Check for executed work (summary.md files)
|
|
801
|
-
if (targetDir && !force) {
|
|
802
|
-
const targetPath = path.join(phasesDir, targetDir);
|
|
803
|
-
const files = fs.readdirSync(targetPath);
|
|
804
|
-
const summaries = files.filter(isSummaryFile);
|
|
805
|
-
if (summaries.length > 0) {
|
|
806
|
-
error(`Phase ${targetPhase} has ${summaries.length} executed plan(s). Use --force to remove anyway.`);
|
|
807
|
-
}
|
|
808
|
-
}
|
|
809
|
-
|
|
810
|
-
// Delete target directory
|
|
811
|
-
let removeWarning = null;
|
|
812
|
-
if (targetDir) {
|
|
813
|
-
const removeResult = removePhaseFromDisk(path.join(phasesDir, targetDir));
|
|
814
|
-
if (!removeResult.removed) {
|
|
815
|
-
removeWarning = removeResult.error;
|
|
816
|
-
}
|
|
817
|
-
}
|
|
818
|
-
|
|
819
|
-
// Renumber subsequent phases using the appropriate strategy
|
|
820
|
-
let renamedDirs = [];
|
|
821
|
-
let renamedFiles = [];
|
|
822
|
-
|
|
823
|
-
let renameError = null;
|
|
824
|
-
if (isDecimal) {
|
|
825
|
-
// Decimal removal: renumber sibling decimals (e.g., removing 06.2 -> 06.3 becomes 06.2)
|
|
826
|
-
const baseParts = normalized.split('.');
|
|
827
|
-
const baseInt = baseParts[0];
|
|
828
|
-
const removedDecimal = parseInt(baseParts[1], 10);
|
|
829
|
-
const result = renumberDecimalPhases(phasesDir, baseInt, removedDecimal);
|
|
830
|
-
renamedDirs = result.renamedDirs;
|
|
831
|
-
renamedFiles = result.renamedFiles;
|
|
832
|
-
renameError = result.error || null;
|
|
833
|
-
} else {
|
|
834
|
-
// Integer removal: renumber all subsequent integer phases
|
|
835
|
-
const removedInt = parseInt(normalized, 10);
|
|
836
|
-
const result = renumberIntegerPhases(phasesDir, removedInt);
|
|
837
|
-
renamedDirs = result.renamedDirs;
|
|
838
|
-
renamedFiles = result.renamedFiles;
|
|
839
|
-
renameError = result.error || null;
|
|
840
|
-
}
|
|
841
|
-
|
|
842
|
-
// Update roadmap.md: remove section and renumber references
|
|
843
|
-
updateRoadmapAfterRemoval(cwd, targetPhase, isDecimal, normalized);
|
|
844
|
-
|
|
845
|
-
// Update state.md phase count
|
|
846
|
-
const stateUpdated = updateStateAfterPhaseRemoval(cwd);
|
|
847
|
-
|
|
848
|
-
const result = {
|
|
849
|
-
removed: targetPhase,
|
|
850
|
-
directory_deleted: targetDir || null,
|
|
851
|
-
renamed_directories: renamedDirs,
|
|
852
|
-
renamed_files: renamedFiles,
|
|
853
|
-
roadmap_updated: true,
|
|
854
|
-
state_updated: stateUpdated,
|
|
855
|
-
};
|
|
856
|
-
if (renameError) result.rename_warning = renameError;
|
|
857
|
-
if (removeWarning) result.remove_warning = removeWarning;
|
|
858
|
-
|
|
859
|
-
output(result, raw);
|
|
860
|
-
}
|
|
861
|
-
|
|
862
|
-
/**
|
|
863
|
-
* Decrement phase count in state.md after a phase is removed.
|
|
864
|
-
* @param {string} cwd - Working directory path
|
|
865
|
-
* @returns {boolean} true if state.md was updated
|
|
866
|
-
*/
|
|
867
|
-
function updateStateAfterPhaseRemoval(cwd) {
|
|
868
|
-
const statePath = path.join(planningPath(cwd), STATE_FILE);
|
|
869
|
-
const content = readStateSafe(statePath);
|
|
870
|
-
if (content === null) return false;
|
|
871
|
-
|
|
872
|
-
let updated = content;
|
|
873
|
-
// Decrement "Total Phases" field
|
|
874
|
-
const totalPattern = /(\*\*Total Phases:\*\*\s*)(\d+)/;
|
|
875
|
-
const totalMatch = updated.match(totalPattern);
|
|
876
|
-
if (totalMatch) {
|
|
877
|
-
updated = updated.replace(totalPattern, `$1${parseInt(totalMatch[2], 10) - 1}`);
|
|
878
|
-
}
|
|
879
|
-
// Decrement "Phase: X of Y" pattern
|
|
880
|
-
const ofPattern = /(\bof\s+)(\d+)(\s*(?:\(|phases?))/i;
|
|
881
|
-
const ofMatch = updated.match(ofPattern);
|
|
882
|
-
if (ofMatch) {
|
|
883
|
-
updated = updated.replace(ofPattern, `$1${parseInt(ofMatch[2], 10) - 1}$3`);
|
|
884
|
-
}
|
|
885
|
-
writeStateMd(statePath, updated, cwd);
|
|
886
|
-
return true;
|
|
887
|
-
}
|
|
522
|
+
// ─── Phase removal — extracted to phase-remove.cjs (re-exported below) ──────
|
|
888
523
|
|
|
889
524
|
// ─── cmdPhaseComplete helpers ───────────────────────────────────────────────
|
|
890
525
|
|
|
@@ -313,6 +313,11 @@ function runExperiment(slug, opts = {}) {
|
|
|
313
313
|
output_tokens: envelope.usage?.output_tokens ?? null,
|
|
314
314
|
cache_creation_input_tokens: envelope.usage?.cache_creation_input_tokens ?? null,
|
|
315
315
|
cache_read_input_tokens: envelope.usage?.cache_read_input_tokens ?? null,
|
|
316
|
+
// Headless `claude -p` usage bills against the Claude Agent SDK
|
|
317
|
+
// credit pool — separate from interactive subscription limits since
|
|
318
|
+
// June 15, 2026. Tagged so /pan:learn and billing reconciliation can
|
|
319
|
+
// separate experiment spend from interactive-session spend.
|
|
320
|
+
billing_pool: (manifest.runtime === 'claude') ? 'agent_sdk' : null,
|
|
316
321
|
};
|
|
317
322
|
appendEvent(runState, 'metrics_captured', `cost=$${envelope.total_cost_usd ?? '?'}, turns=${envelope.num_turns ?? '?'}`);
|
|
318
323
|
} else {
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Squads — agent groupings for the bot-army model (ADR-0032).
|
|
3
|
+
*
|
|
4
|
+
* A squad is a named, tool-scoped, model-tiered grouping of existing PAN
|
|
5
|
+
* agents under the pan-conductor coordinator. This module is a registry +
|
|
6
|
+
* resolver only — it modifies no agent and changes no execution path. The
|
|
7
|
+
* army campaign command (ADR-0033) consumes it; until then it is inert.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
'use strict';
|
|
11
|
+
|
|
12
|
+
const { output, error } = require('./core.cjs');
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Coordinator + worker/utility agents that are NOT squad members.
|
|
16
|
+
* - coordinator: the top of the hierarchy (Tier 0).
|
|
17
|
+
* - workers: cheap narrow-job agents (Tier 2) + standalone utility agents
|
|
18
|
+
* invoked directly by commands, not delegated through a squad.
|
|
19
|
+
*/
|
|
20
|
+
const COORDINATOR = 'pan-conductor';
|
|
21
|
+
const WORKERS = Object.freeze([
|
|
22
|
+
'pan-document_code', // Haiku-tier codebase mapper
|
|
23
|
+
'pan-distiller', // Haiku-tier code-bloat optimizer
|
|
24
|
+
'pan-optimizer', // optimization loop
|
|
25
|
+
'pan-experiment-runner', // self-improvement loop
|
|
26
|
+
'pan-knowledge', // retrieval/Q&A
|
|
27
|
+
'pan-counterfactual', // what-if worktree replay
|
|
28
|
+
'pan-previewer', // foresight synthesis
|
|
29
|
+
]);
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* The four squads, keyed by lifecycle role. `tier` is a PAN model tier
|
|
33
|
+
* (resolve-model maps it to a provider model); `access` is the least-privilege
|
|
34
|
+
* tool contract the conductor grants when delegating to the squad.
|
|
35
|
+
*/
|
|
36
|
+
const SQUADS = Object.freeze({
|
|
37
|
+
architecture: Object.freeze({
|
|
38
|
+
label: 'Architecture',
|
|
39
|
+
tier: 'reasoning',
|
|
40
|
+
access: 'read-only',
|
|
41
|
+
summary: 'Designs the system before code — contract-first.',
|
|
42
|
+
agents: Object.freeze([
|
|
43
|
+
'pan-roadmapper', 'pan-planner', 'pan-plan-checker',
|
|
44
|
+
'pan-project-researcher', 'pan-phase-researcher', 'pan-research-synthesizer',
|
|
45
|
+
]),
|
|
46
|
+
}),
|
|
47
|
+
build: Object.freeze({
|
|
48
|
+
label: 'Build',
|
|
49
|
+
tier: 'reasoning',
|
|
50
|
+
access: 'read-write-bash',
|
|
51
|
+
summary: 'Turns design and contracts into committed code.',
|
|
52
|
+
agents: Object.freeze(['pan-executor']),
|
|
53
|
+
}),
|
|
54
|
+
quality: Object.freeze({
|
|
55
|
+
label: 'Quality',
|
|
56
|
+
tier: 'mid',
|
|
57
|
+
access: 'read-only',
|
|
58
|
+
summary: 'Adversarially breaks what Build makes before users do.',
|
|
59
|
+
agents: Object.freeze([
|
|
60
|
+
'pan-reviewer', 'pan-hardener', 'pan-meta-reviewer',
|
|
61
|
+
'pan-verifier', 'pan-integration-checker', 'pan-debugger',
|
|
62
|
+
]),
|
|
63
|
+
}),
|
|
64
|
+
release: Object.freeze({
|
|
65
|
+
label: 'Release',
|
|
66
|
+
tier: 'mid',
|
|
67
|
+
access: 'always-ask',
|
|
68
|
+
summary: 'Ships safely behind a human gate; rolls back fast.',
|
|
69
|
+
agents: Object.freeze(['pan-release']),
|
|
70
|
+
}),
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
const SQUAD_NAMES = Object.freeze(Object.keys(SQUADS));
|
|
74
|
+
|
|
75
|
+
/** @returns {Array<{name, label, tier, access, summary, agent_count}>} */
|
|
76
|
+
function listSquads() {
|
|
77
|
+
return SQUAD_NAMES.map(name => {
|
|
78
|
+
const s = SQUADS[name];
|
|
79
|
+
return { name, label: s.label, tier: s.tier, access: s.access, summary: s.summary, agent_count: s.agents.length };
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** @returns {object|null} the squad record (with name) or null if unknown. */
|
|
84
|
+
function getSquad(name) {
|
|
85
|
+
const s = SQUADS[name];
|
|
86
|
+
return s ? { name, ...s, agents: [...s.agents] } : null;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** Reverse lookup: which squad owns an agent? @returns {string|null} */
|
|
90
|
+
function squadForAgent(agent) {
|
|
91
|
+
for (const name of SQUAD_NAMES) {
|
|
92
|
+
if (SQUADS[name].agents.includes(agent)) return name;
|
|
93
|
+
}
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Validate the roster against the set of real agents.
|
|
99
|
+
* @param {string[]} knownAgents - agent names that exist on disk
|
|
100
|
+
* @returns {{ ok: boolean, missing: string[], unassigned: string[] }}
|
|
101
|
+
* missing = squad members with no agent file
|
|
102
|
+
* unassigned = real agents that are neither coordinator, worker, nor squad member
|
|
103
|
+
*/
|
|
104
|
+
function validateRoster(knownAgents) {
|
|
105
|
+
const known = new Set(knownAgents);
|
|
106
|
+
const missing = [];
|
|
107
|
+
for (const name of SQUAD_NAMES) {
|
|
108
|
+
for (const a of SQUADS[name].agents) {
|
|
109
|
+
if (!known.has(a)) missing.push(a);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
const assigned = new Set([COORDINATOR, ...WORKERS]);
|
|
113
|
+
for (const name of SQUAD_NAMES) for (const a of SQUADS[name].agents) assigned.add(a);
|
|
114
|
+
const unassigned = knownAgents.filter(a => !assigned.has(a));
|
|
115
|
+
return { ok: missing.length === 0 && unassigned.length === 0, missing, unassigned };
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// ─── CLI ─────────────────────────────────────────────────────────────────────
|
|
119
|
+
|
|
120
|
+
function cmdSquadList(raw) {
|
|
121
|
+
const squads = listSquads();
|
|
122
|
+
const human = squads
|
|
123
|
+
.map(s => `${s.label.padEnd(13)} ${s.tier.padEnd(10)} ${s.access.padEnd(16)} ${s.agent_count} agents`)
|
|
124
|
+
.join('\n');
|
|
125
|
+
output({ squads, coordinator: COORDINATOR, workers: [...WORKERS] }, raw, human);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function cmdSquadShow(name, raw) {
|
|
129
|
+
const s = getSquad(name);
|
|
130
|
+
if (!s) {
|
|
131
|
+
return error(`Unknown squad "${name}". Available: ${SQUAD_NAMES.join(', ')}`);
|
|
132
|
+
}
|
|
133
|
+
const human = [
|
|
134
|
+
`${s.label} squad — ${s.tier} tier · ${s.access}`,
|
|
135
|
+
s.summary,
|
|
136
|
+
s.agents.length ? 'Agents: ' + s.agents.join(', ') : 'Agents: (none — git-tool driven)',
|
|
137
|
+
].join('\n');
|
|
138
|
+
output(s, raw, human);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
module.exports = {
|
|
142
|
+
SQUADS,
|
|
143
|
+
SQUAD_NAMES,
|
|
144
|
+
COORDINATOR,
|
|
145
|
+
WORKERS,
|
|
146
|
+
listSquads,
|
|
147
|
+
getSquad,
|
|
148
|
+
squadForAgent,
|
|
149
|
+
validateRoster,
|
|
150
|
+
cmdSquadList,
|
|
151
|
+
cmdSquadShow,
|
|
152
|
+
};
|
|
@@ -6,6 +6,7 @@ const fs = require('fs');
|
|
|
6
6
|
const path = require('path');
|
|
7
7
|
const { loadConfig, getMilestoneInfo, escapeRegex, safeReadFile, output, error } = require('./core.cjs');
|
|
8
8
|
const { extractFrontmatter, reconstructFrontmatter } = require('./frontmatter.cjs');
|
|
9
|
+
const { withFileLock, writeFileAtomic } = require('./lock.cjs');
|
|
9
10
|
const {
|
|
10
11
|
PLANNING_DIR,
|
|
11
12
|
STATE_FILE,
|
|
@@ -855,11 +856,19 @@ function syncStateFrontmatter(content, cwd) {
|
|
|
855
856
|
/**
|
|
856
857
|
* Write state.md with synchronized YAML frontmatter.
|
|
857
858
|
* All state.md writes should use this instead of raw writeFileSync.
|
|
859
|
+
*
|
|
860
|
+
* Concurrency (ADR-0030): the write is serialized behind an advisory
|
|
861
|
+
* state.md.lock and lands atomically (temp + rename), so concurrent agents
|
|
862
|
+
* cannot tear the file or interleave read-modify-write cycles. Lock
|
|
863
|
+
* acquisition is best-effort — on timeout the write proceeds unlocked,
|
|
864
|
+
* preserving single-agent behavior exactly.
|
|
858
865
|
*/
|
|
859
866
|
function writeStateMd(statePath, content, cwd) {
|
|
860
867
|
const synced = syncStateFrontmatter(content, cwd);
|
|
861
868
|
try {
|
|
862
|
-
|
|
869
|
+
withFileLock(statePath, () => {
|
|
870
|
+
writeFileAtomic(statePath, synced);
|
|
871
|
+
});
|
|
863
872
|
} catch (err) {
|
|
864
873
|
throw new Error('Failed to write state.md: ' + err.message);
|
|
865
874
|
}
|