@adaptic/maestro 1.4.1 → 1.5.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/bin/maestro.mjs +257 -55
- package/package.json +1 -1
package/bin/maestro.mjs
CHANGED
|
@@ -8,16 +8,21 @@
|
|
|
8
8
|
* npx @adaptic/maestro doctor # Verify installation and configuration
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
import { resolve, join, dirname } from "node:path";
|
|
11
|
+
import { resolve, join, dirname, relative, sep } from "node:path";
|
|
12
12
|
import { fileURLToPath } from "node:url";
|
|
13
13
|
import {
|
|
14
14
|
mkdirSync,
|
|
15
15
|
cpSync,
|
|
16
|
+
copyFileSync,
|
|
16
17
|
existsSync,
|
|
17
18
|
readFileSync,
|
|
18
19
|
writeFileSync,
|
|
20
|
+
readdirSync,
|
|
21
|
+
statSync,
|
|
22
|
+
lstatSync,
|
|
19
23
|
} from "node:fs";
|
|
20
24
|
import { execFileSync } from "node:child_process";
|
|
25
|
+
import { createHash } from "node:crypto";
|
|
21
26
|
|
|
22
27
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
23
28
|
const MAESTRO_ROOT = resolve(__dirname, "..");
|
|
@@ -664,68 +669,242 @@ rubrics: []
|
|
|
664
669
|
}
|
|
665
670
|
|
|
666
671
|
// ---------------------------------------------------------------------------
|
|
667
|
-
// UPGRADE — update framework files in current agent repo
|
|
672
|
+
// UPGRADE — update framework files in current agent repo (smart merge)
|
|
668
673
|
// ---------------------------------------------------------------------------
|
|
674
|
+
//
|
|
675
|
+
// Upgrade philosophy: by default, never silently overwrite a file the user has
|
|
676
|
+
// modified locally. Detection uses git as the source of truth:
|
|
677
|
+
//
|
|
678
|
+
// • untracked or has-uncommitted-changes in agent repo ⇒ "locally modified"
|
|
679
|
+
// • tracked and clean against HEAD ⇒ "vendored, safe to overwrite"
|
|
680
|
+
//
|
|
681
|
+
// For every framework file we'd otherwise overwrite, we classify into:
|
|
682
|
+
//
|
|
683
|
+
// added — file doesn't exist in agent repo → copy
|
|
684
|
+
// updated — file exists, agent's copy matches HEAD (no local edits) → overwrite
|
|
685
|
+
// same — file exists, content already byte-identical to upstream → skip
|
|
686
|
+
// preserved — file exists with local edits → keep agent's copy, write the
|
|
687
|
+
// upstream version to .maestro/incoming/<path> for manual review
|
|
688
|
+
// (unless --force-overwrite is passed)
|
|
689
|
+
//
|
|
690
|
+
// Flags:
|
|
691
|
+
// --dry-run preview only; no files written
|
|
692
|
+
// --force-overwrite overwrite even locally-modified files (with backup)
|
|
693
|
+
// --no-incoming skip writing .maestro/incoming/ shadows
|
|
694
|
+
// --verbose list every file's classification
|
|
695
|
+
|
|
696
|
+
// Paths that get upgraded. Each entry is { path, mode } where mode is:
|
|
697
|
+
// "smart" — per-file classification described above
|
|
698
|
+
// "merge" — add new files only; never overwrite existing (used for agents/)
|
|
699
|
+
const UPGRADE_PATHS = [
|
|
700
|
+
{ path: "scripts", mode: "smart" },
|
|
701
|
+
{ path: "policies", mode: "smart" },
|
|
702
|
+
{ path: "docs", mode: "smart" },
|
|
703
|
+
{ path: "public/assets", mode: "smart" },
|
|
704
|
+
{ path: "workflows", mode: "smart" },
|
|
705
|
+
{ path: "schedules", mode: "smart" },
|
|
706
|
+
{ path: "desktop-control", mode: "smart" },
|
|
707
|
+
{ path: "ingest", mode: "smart" },
|
|
708
|
+
{ path: "mcp", mode: "smart" },
|
|
709
|
+
{ path: ".claude/commands", mode: "smart" },
|
|
710
|
+
{ path: "plugins/maestro-skills", mode: "smart" },
|
|
711
|
+
{ path: "teams", mode: "smart" },
|
|
712
|
+
{ path: "agents", mode: "merge" },
|
|
713
|
+
];
|
|
714
|
+
|
|
715
|
+
function sha256File(p) {
|
|
716
|
+
return createHash("sha256").update(readFileSync(p)).digest("hex");
|
|
717
|
+
}
|
|
669
718
|
|
|
670
|
-
function
|
|
671
|
-
const
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
719
|
+
function walkFiles(root) {
|
|
720
|
+
const out = [];
|
|
721
|
+
if (!existsSync(root)) return out;
|
|
722
|
+
const stack = [root];
|
|
723
|
+
while (stack.length) {
|
|
724
|
+
const dir = stack.pop();
|
|
725
|
+
for (const name of readdirSync(dir)) {
|
|
726
|
+
const full = join(dir, name);
|
|
727
|
+
// Use lstat first so we can detect (and skip) symlinks — particularly
|
|
728
|
+
// broken ones, which are common in shared scaffolds.
|
|
729
|
+
let lst;
|
|
730
|
+
try { lst = lstatSync(full); }
|
|
731
|
+
catch { continue; }
|
|
732
|
+
if (lst.isSymbolicLink()) {
|
|
733
|
+
// Skip symlinks entirely — agents shouldn't inherit links into paths
|
|
734
|
+
// that may not exist on the target machine.
|
|
735
|
+
continue;
|
|
736
|
+
}
|
|
737
|
+
if (lst.isDirectory()) stack.push(full);
|
|
738
|
+
else if (lst.isFile()) out.push(full);
|
|
739
|
+
}
|
|
676
740
|
}
|
|
741
|
+
return out;
|
|
742
|
+
}
|
|
677
743
|
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
];
|
|
744
|
+
function isGitRepo(cwd) {
|
|
745
|
+
try {
|
|
746
|
+
execFileSync("git", ["rev-parse", "--is-inside-work-tree"], { cwd, stdio: "pipe" });
|
|
747
|
+
return true;
|
|
748
|
+
} catch { return false; }
|
|
749
|
+
}
|
|
685
750
|
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
751
|
+
// Build a Set of repo-relative paths that are dirty (modified, added, untracked).
|
|
752
|
+
// Single git invocation is much faster than per-file checks.
|
|
753
|
+
function dirtyPathSet(cwd) {
|
|
754
|
+
const set = new Set();
|
|
755
|
+
try {
|
|
756
|
+
const out = execFileSync(
|
|
757
|
+
"git",
|
|
758
|
+
["status", "--porcelain=v1", "-z", "--untracked-files=all"],
|
|
759
|
+
{ cwd, encoding: "utf-8" }
|
|
760
|
+
);
|
|
761
|
+
// -z separates records with NUL; each record is "XY path" (no quoting).
|
|
762
|
+
// For renames (R/C) the format is "R newpath\0oldpath\0" — we only care
|
|
763
|
+
// about destination, but for safety we treat both as dirty.
|
|
764
|
+
for (const rec of out.split("\0")) {
|
|
765
|
+
if (!rec) continue;
|
|
766
|
+
const status = rec.slice(0, 2);
|
|
767
|
+
const path = rec.slice(3);
|
|
768
|
+
// Anything in porcelain output is non-clean by definition.
|
|
769
|
+
if (status !== " ") set.add(path);
|
|
693
770
|
}
|
|
771
|
+
} catch {
|
|
772
|
+
// Not a git repo or git failed — caller will fall back to caution mode.
|
|
694
773
|
}
|
|
774
|
+
return set;
|
|
775
|
+
}
|
|
695
776
|
|
|
696
|
-
|
|
697
|
-
const
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
777
|
+
function parseUpgradeFlags(args) {
|
|
778
|
+
const flags = {
|
|
779
|
+
dryRun: false,
|
|
780
|
+
forceOverwrite: false,
|
|
781
|
+
noIncoming: false,
|
|
782
|
+
verbose: false,
|
|
783
|
+
};
|
|
784
|
+
for (const a of args) {
|
|
785
|
+
if (a === "--dry-run" || a === "-n") flags.dryRun = true;
|
|
786
|
+
else if (a === "--force-overwrite" || a === "--force") flags.forceOverwrite = true;
|
|
787
|
+
else if (a === "--no-incoming") flags.noIncoming = true;
|
|
788
|
+
else if (a === "--verbose" || a === "-v") flags.verbose = true;
|
|
789
|
+
else if (a === "--help" || a === "-h") return null;
|
|
790
|
+
else { fail(`Unknown flag: ${a}`); process.exit(1); }
|
|
702
791
|
}
|
|
792
|
+
return flags;
|
|
793
|
+
}
|
|
703
794
|
|
|
704
|
-
|
|
705
|
-
const
|
|
706
|
-
if (
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
795
|
+
function upgrade(args = []) {
|
|
796
|
+
const flags = parseUpgradeFlags(args);
|
|
797
|
+
if (flags === null) {
|
|
798
|
+
console.log(`
|
|
799
|
+
Usage: maestro upgrade [flags]
|
|
800
|
+
|
|
801
|
+
Flags:
|
|
802
|
+
--dry-run, -n Preview changes without writing
|
|
803
|
+
--force-overwrite Overwrite even locally-modified files (backs them up)
|
|
804
|
+
--no-incoming Don't write .maestro/incoming/ shadows for preserved files
|
|
805
|
+
--verbose, -v Print classification for every file
|
|
806
|
+
--help, -h Show this help
|
|
807
|
+
`);
|
|
808
|
+
return;
|
|
710
809
|
}
|
|
711
810
|
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
if (existsSync(
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
811
|
+
const cwd = process.cwd();
|
|
812
|
+
|
|
813
|
+
if (!existsSync(join(cwd, "config/agent.ts")) && !existsSync(join(cwd, "CLAUDE.md"))) {
|
|
814
|
+
fail("Not a Maestro agent directory (config/agent.ts not found)");
|
|
815
|
+
process.exit(1);
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
const inGit = isGitRepo(cwd);
|
|
819
|
+
if (!inGit) {
|
|
820
|
+
warn("Not in a git repo — cannot detect local modifications.");
|
|
821
|
+
warn("All existing files will be treated as locally modified and preserved.");
|
|
822
|
+
warn("Use --force-overwrite to override.");
|
|
718
823
|
}
|
|
719
824
|
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
825
|
+
const dirty = inGit ? dirtyPathSet(cwd) : null;
|
|
826
|
+
|
|
827
|
+
const banner = flags.dryRun ? "DRY RUN — " : "";
|
|
828
|
+
log(`${banner}Upgrading framework files from @adaptic/maestro...`);
|
|
829
|
+
|
|
830
|
+
const counts = { added: 0, updated: 0, same: 0, preserved: 0, mergeKept: 0, forced: 0 };
|
|
831
|
+
const preservedFiles = [];
|
|
832
|
+
|
|
833
|
+
for (const { path: relRoot, mode } of UPGRADE_PATHS) {
|
|
834
|
+
const srcRoot = join(MAESTRO_ROOT, relRoot);
|
|
835
|
+
const dstRoot = join(cwd, relRoot);
|
|
836
|
+
if (!existsSync(srcRoot)) continue;
|
|
837
|
+
|
|
838
|
+
const srcFiles = walkFiles(srcRoot);
|
|
839
|
+
for (const srcFile of srcFiles) {
|
|
840
|
+
const relFromRoot = relative(srcRoot, srcFile);
|
|
841
|
+
const dstFile = join(dstRoot, relFromRoot);
|
|
842
|
+
const repoRel = relative(cwd, dstFile).split(sep).join("/");
|
|
843
|
+
|
|
844
|
+
// Case 1: new file (doesn't exist in agent) — always copy.
|
|
845
|
+
if (!existsSync(dstFile)) {
|
|
846
|
+
if (!flags.dryRun) {
|
|
847
|
+
mkdirSync(dirname(dstFile), { recursive: true });
|
|
848
|
+
copyFileSync(srcFile, dstFile);
|
|
849
|
+
}
|
|
850
|
+
counts.added++;
|
|
851
|
+
if (flags.verbose) ok(`+ ${repoRel}`);
|
|
852
|
+
continue;
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
// Case 2: byte-identical — nothing to do.
|
|
856
|
+
const srcHash = sha256File(srcFile);
|
|
857
|
+
const dstHash = sha256File(dstFile);
|
|
858
|
+
if (srcHash === dstHash) {
|
|
859
|
+
counts.same++;
|
|
860
|
+
if (flags.verbose) console.log(` = ${repoRel}`);
|
|
861
|
+
continue;
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
// Case 3: merge-mode never overwrites existing files.
|
|
865
|
+
if (mode === "merge") {
|
|
866
|
+
counts.mergeKept++;
|
|
867
|
+
if (flags.verbose) console.log(` ~ ${repoRel} (merge-mode, kept)`);
|
|
868
|
+
continue;
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
// Case 4: locally modified? Determine via git dirty set.
|
|
872
|
+
const locallyModified = inGit ? dirty.has(repoRel) : true;
|
|
873
|
+
|
|
874
|
+
if (locallyModified && !flags.forceOverwrite) {
|
|
875
|
+
counts.preserved++;
|
|
876
|
+
preservedFiles.push(repoRel);
|
|
877
|
+
if (!flags.dryRun && !flags.noIncoming) {
|
|
878
|
+
const shadow = join(cwd, ".maestro", "incoming", repoRel);
|
|
879
|
+
mkdirSync(dirname(shadow), { recursive: true });
|
|
880
|
+
copyFileSync(srcFile, shadow);
|
|
881
|
+
}
|
|
882
|
+
if (flags.verbose) console.log(` ~ ${repoRel} (local edits, preserved)`);
|
|
883
|
+
continue;
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
// Case 5: overwrite (clean tracked file, or --force).
|
|
887
|
+
if (locallyModified && flags.forceOverwrite) {
|
|
888
|
+
// Back up the about-to-be-clobbered file.
|
|
889
|
+
if (!flags.dryRun) {
|
|
890
|
+
const backup = join(cwd, ".maestro", "backup", repoRel);
|
|
891
|
+
mkdirSync(dirname(backup), { recursive: true });
|
|
892
|
+
copyFileSync(dstFile, backup);
|
|
893
|
+
}
|
|
894
|
+
counts.forced++;
|
|
895
|
+
if (flags.verbose) warn(`! ${repoRel} (forced; backup written)`);
|
|
896
|
+
} else {
|
|
897
|
+
counts.updated++;
|
|
898
|
+
if (flags.verbose) console.log(` > ${repoRel}`);
|
|
899
|
+
}
|
|
900
|
+
if (!flags.dryRun) {
|
|
901
|
+
mkdirSync(dirname(dstFile), { recursive: true });
|
|
902
|
+
copyFileSync(srcFile, dstFile);
|
|
903
|
+
}
|
|
904
|
+
}
|
|
726
905
|
}
|
|
727
906
|
|
|
728
|
-
//
|
|
907
|
+
// Ensure standard runtime directories exist.
|
|
729
908
|
const ensureDirs = [
|
|
730
909
|
"state/handoffs", "state/huddle", "state/indexes", "state/rag",
|
|
731
910
|
"state/sessions", "state/slack-responded", "state/slack-thread-tracker",
|
|
@@ -739,15 +918,32 @@ function upgrade() {
|
|
|
739
918
|
for (const dir of ensureDirs) {
|
|
740
919
|
const dirPath = join(cwd, dir);
|
|
741
920
|
if (!existsSync(dirPath)) {
|
|
742
|
-
mkdirSync(dirPath, { recursive: true });
|
|
921
|
+
if (!flags.dryRun) mkdirSync(dirPath, { recursive: true });
|
|
743
922
|
newDirs++;
|
|
744
923
|
}
|
|
745
924
|
}
|
|
746
|
-
|
|
925
|
+
|
|
926
|
+
// Summary
|
|
927
|
+
console.log();
|
|
928
|
+
console.log(`${C.bold}Upgrade summary${C.reset}${flags.dryRun ? " (dry run, nothing written)" : ""}`);
|
|
929
|
+
if (counts.added) ok(`${counts.added} added (new files)`);
|
|
930
|
+
if (counts.updated) ok(`${counts.updated} updated (vendored, no local edits)`);
|
|
931
|
+
if (counts.same) console.log(` = ${counts.same} already up-to-date`);
|
|
932
|
+
if (counts.mergeKept) console.log(` ~ ${counts.mergeKept} merge-mode kept (agents/ custom files preserved)`);
|
|
933
|
+
if (counts.preserved) warn(`${counts.preserved} preserved (local edits — kept your version)`);
|
|
934
|
+
if (counts.forced) warn(`${counts.forced} force-overwritten (backups in .maestro/backup/)`);
|
|
935
|
+
if (newDirs) ok(`${newDirs} new directories created`);
|
|
936
|
+
|
|
937
|
+
if (preservedFiles.length && !flags.noIncoming && !flags.dryRun) {
|
|
938
|
+
console.log();
|
|
939
|
+
log("Upstream versions of your locally-modified files saved to .maestro/incoming/");
|
|
940
|
+
log("Review with: diff <path> .maestro/incoming/<path>");
|
|
941
|
+
for (const p of preservedFiles.slice(0, 5)) console.log(` ${p}`);
|
|
942
|
+
if (preservedFiles.length > 5) console.log(` … and ${preservedFiles.length - 5} more`);
|
|
943
|
+
}
|
|
747
944
|
|
|
748
945
|
console.log();
|
|
749
|
-
|
|
750
|
-
log("Agent-specific files (config/, CLAUDE.md, knowledge/, memory/) were NOT modified.");
|
|
946
|
+
log("Agent-specific paths (config/, CLAUDE.md, knowledge/, memory/, state/, outputs/, logs/, .env) were NOT touched.");
|
|
751
947
|
}
|
|
752
948
|
|
|
753
949
|
// ---------------------------------------------------------------------------
|
|
@@ -820,16 +1016,22 @@ const [, , command, ...args] = process.argv;
|
|
|
820
1016
|
|
|
821
1017
|
switch (command) {
|
|
822
1018
|
case "create": create(args[0]); break;
|
|
823
|
-
case "upgrade": upgrade(); break;
|
|
1019
|
+
case "upgrade": upgrade(args); break;
|
|
824
1020
|
case "doctor": doctor(); break;
|
|
825
1021
|
case "--help": case "-h": case undefined:
|
|
826
1022
|
console.log(`
|
|
827
1023
|
Maestro — Autonomous AI Agent Operating System
|
|
828
1024
|
|
|
829
1025
|
Usage:
|
|
830
|
-
npx @adaptic/maestro create <dirname>
|
|
831
|
-
npx @adaptic/maestro upgrade
|
|
832
|
-
npx @adaptic/maestro doctor
|
|
1026
|
+
npx @adaptic/maestro create <dirname> Create a new agent repo
|
|
1027
|
+
npx @adaptic/maestro upgrade [--dry-run] Update framework files
|
|
1028
|
+
npx @adaptic/maestro doctor Verify installation
|
|
1029
|
+
|
|
1030
|
+
Upgrade flags:
|
|
1031
|
+
--dry-run, -n Preview changes without writing
|
|
1032
|
+
--force-overwrite Overwrite even locally-modified files (backs them up)
|
|
1033
|
+
--no-incoming Don't write .maestro/incoming/ shadows
|
|
1034
|
+
--verbose, -v List classification for every file
|
|
833
1035
|
|
|
834
1036
|
Workflow:
|
|
835
1037
|
1. npx @adaptic/maestro create my-agent
|