@adaptic/maestro 1.5.0 → 1.5.2

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.
Files changed (2) hide show
  1. package/bin/maestro.mjs +94 -1
  2. package/package.json +1 -1
package/bin/maestro.mjs CHANGED
@@ -741,6 +741,64 @@ function walkFiles(root) {
741
741
  return out;
742
742
  }
743
743
 
744
+ // .maestroignore — gitignore-style allowlist of paths the upgrade must NOT
745
+ // touch. Supports comments (#), directory prefixes (trailing /), exact paths,
746
+ // simple globs (* matches a single path segment, ** matches recursively), and
747
+ // negation (! prefix to un-ignore). Match order is top-to-bottom; last match
748
+ // wins, like .gitignore.
749
+ function loadMaestroignore(cwd) {
750
+ const file = join(cwd, ".maestroignore");
751
+ if (!existsSync(file)) return null;
752
+ const patterns = [];
753
+ for (const raw of readFileSync(file, "utf-8").split(/\r?\n/)) {
754
+ const stripped = raw.replace(/^\s+|\s+$/g, "");
755
+ if (!stripped || stripped.startsWith("#")) continue;
756
+ const negate = stripped.startsWith("!");
757
+ const pat = negate ? stripped.slice(1).trim() : stripped;
758
+ if (!pat) continue;
759
+ patterns.push({ pat, negate });
760
+ }
761
+ return patterns.length ? patterns : null;
762
+ }
763
+
764
+ function patternToRegex(pat) {
765
+ // Helper: convert a glob-aware pattern to its regex source.
766
+ // ** must be substituted before single * so we don't double-rewrite it.
767
+ // * is intentionally outside the regex-escape char class so the
768
+ // glob substitution can find unescaped `*` characters afterward.
769
+ const globToRe = (p) =>
770
+ p.replace(/[.+^${}()|[\]\\]/g, "\\$&")
771
+ .replace(/\*\*/g, "<DBL>")
772
+ .replace(/\*/g, "[^/]*")
773
+ .replace(/<DBL>/g, ".*");
774
+
775
+ // Directory prefix: `scripts/daemon/` or `agents/sophie-*/` match
776
+ // anything under that path (recursive).
777
+ if (pat.endsWith("/")) {
778
+ return new RegExp("^" + globToRe(pat));
779
+ }
780
+ // Single-line glob: must match in full (no trailing slash).
781
+ if (pat.includes("*")) {
782
+ return new RegExp("^" + globToRe(pat) + "$");
783
+ }
784
+ // Exact: also matches anything underneath (gitignore semantics — a
785
+ // pattern without trailing slash still protects a directory if it
786
+ // resolves to one).
787
+ const escaped = pat.replace(/[.+^${}()|[\]\\]/g, "\\$&");
788
+ return new RegExp("^" + escaped + "(/|$)");
789
+ }
790
+
791
+ function matchesIgnore(repoRel, patterns) {
792
+ if (!patterns) return false;
793
+ let ignored = false;
794
+ for (const { pat, negate } of patterns) {
795
+ if (patternToRegex(pat).test(repoRel)) {
796
+ ignored = !negate;
797
+ }
798
+ }
799
+ return ignored;
800
+ }
801
+
744
802
  function isGitRepo(cwd) {
745
803
  try {
746
804
  execFileSync("git", ["rev-parse", "--is-inside-work-tree"], { cwd, stdio: "pipe" });
@@ -804,6 +862,23 @@ Flags:
804
862
  --no-incoming Don't write .maestro/incoming/ shadows for preserved files
805
863
  --verbose, -v Print classification for every file
806
864
  --help, -h Show this help
865
+
866
+ Per-file behaviour:
867
+ added — new upstream file → copy
868
+ updated — existed, byte-equal to your committed copy → safe overwrite
869
+ same — existed and already byte-identical to upstream → skip
870
+ ignored — matches a pattern in .maestroignore → never touched (any state)
871
+ preserved — has uncommitted local edits → keep yours, upstream lands at
872
+ .maestro/incoming/<path> for manual diff
873
+ mergeKept — under agents/ → never overwrite (custom agents preserved)
874
+ forced — overwritten by --force-overwrite; backup at .maestro/backup/<path>
875
+
876
+ .maestroignore format (gitignore-style, top-down, last match wins):
877
+ scripts/slack-send.sh exact file
878
+ scripts/daemon/ directory and everything beneath
879
+ scripts/send-email-*.py single-segment glob
880
+ workflows/** recursive glob
881
+ !workflows/quarterly/ un-ignore (force overwrite)
807
882
  `);
808
883
  return;
809
884
  }
@@ -823,12 +898,17 @@ Flags:
823
898
  }
824
899
 
825
900
  const dirty = inGit ? dirtyPathSet(cwd) : null;
901
+ const ignorePatterns = loadMaestroignore(cwd);
902
+ if (ignorePatterns) {
903
+ log(`.maestroignore loaded (${ignorePatterns.length} pattern${ignorePatterns.length === 1 ? "" : "s"})`);
904
+ }
826
905
 
827
906
  const banner = flags.dryRun ? "DRY RUN — " : "";
828
907
  log(`${banner}Upgrading framework files from @adaptic/maestro...`);
829
908
 
830
- const counts = { added: 0, updated: 0, same: 0, preserved: 0, mergeKept: 0, forced: 0 };
909
+ const counts = { added: 0, updated: 0, same: 0, ignored: 0, preserved: 0, mergeKept: 0, forced: 0 };
831
910
  const preservedFiles = [];
911
+ const ignoredFiles = [];
832
912
 
833
913
  for (const { path: relRoot, mode } of UPGRADE_PATHS) {
834
914
  const srcRoot = join(MAESTRO_ROOT, relRoot);
@@ -841,6 +921,18 @@ Flags:
841
921
  const dstFile = join(dstRoot, relFromRoot);
842
922
  const repoRel = relative(cwd, dstFile).split(sep).join("/");
843
923
 
924
+ const isIgnored = matchesIgnore(repoRel, ignorePatterns);
925
+
926
+ // Case 0: .maestroignore match — never touch, regardless of state.
927
+ // Checked BEFORE existsSync so that protected new-files-from-upstream
928
+ // are never silently introduced into the agent repo either.
929
+ if (isIgnored) {
930
+ counts.ignored++;
931
+ ignoredFiles.push(repoRel);
932
+ if (flags.verbose) console.log(` · ${repoRel} (ignored via .maestroignore)`);
933
+ continue;
934
+ }
935
+
844
936
  // Case 1: new file (doesn't exist in agent) — always copy.
845
937
  if (!existsSync(dstFile)) {
846
938
  if (!flags.dryRun) {
@@ -929,6 +1021,7 @@ Flags:
929
1021
  if (counts.added) ok(`${counts.added} added (new files)`);
930
1022
  if (counts.updated) ok(`${counts.updated} updated (vendored, no local edits)`);
931
1023
  if (counts.same) console.log(` = ${counts.same} already up-to-date`);
1024
+ if (counts.ignored) console.log(` · ${counts.ignored} ignored (.maestroignore protected)`);
932
1025
  if (counts.mergeKept) console.log(` ~ ${counts.mergeKept} merge-mode kept (agents/ custom files preserved)`);
933
1026
  if (counts.preserved) warn(`${counts.preserved} preserved (local edits — kept your version)`);
934
1027
  if (counts.forced) warn(`${counts.forced} force-overwritten (backups in .maestro/backup/)`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adaptic/maestro",
3
- "version": "1.5.0",
3
+ "version": "1.5.2",
4
4
  "description": "Maestro — Autonomous AI agent operating system. Deploy AI employees on dedicated Mac minis.",
5
5
  "type": "module",
6
6
  "bin": {