@adaptic/maestro 1.5.0 → 1.5.1

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 +92 -1
  2. package/package.json +1 -1
package/bin/maestro.mjs CHANGED
@@ -741,6 +741,62 @@ 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
+ // Directory prefix: `scripts/daemon/` matches scripts/daemon/* (recursive)
766
+ if (pat.endsWith("/")) {
767
+ const prefix = pat.replace(/[.+^${}()|[\]\\]/g, "\\$&");
768
+ return new RegExp("^" + prefix);
769
+ }
770
+ // Glob with * and **.
771
+ // Step order matters: ** must be captured before single * to preserve
772
+ // recursive semantics. * is intentionally NOT in the regex-escape char
773
+ // class so we can rewrite it after escaping the other specials.
774
+ if (pat.includes("*")) {
775
+ const escaped = pat
776
+ .replace(/[.+^${}()|[\]\\]/g, "\\$&")
777
+ .replace(/\*\*/g, "<DBL>")
778
+ .replace(/\*/g, "[^/]*")
779
+ .replace(/<DBL>/g, ".*");
780
+ return new RegExp("^" + escaped + "$");
781
+ }
782
+ // Exact: also match anything underneath (a path like `scripts/daemon`
783
+ // without trailing slash should still protect the whole directory if
784
+ // it resolves to one — gitignore semantics).
785
+ const escaped = pat.replace(/[.+^${}()|[\]\\]/g, "\\$&");
786
+ return new RegExp("^" + escaped + "(/|$)");
787
+ }
788
+
789
+ function matchesIgnore(repoRel, patterns) {
790
+ if (!patterns) return false;
791
+ let ignored = false;
792
+ for (const { pat, negate } of patterns) {
793
+ if (patternToRegex(pat).test(repoRel)) {
794
+ ignored = !negate;
795
+ }
796
+ }
797
+ return ignored;
798
+ }
799
+
744
800
  function isGitRepo(cwd) {
745
801
  try {
746
802
  execFileSync("git", ["rev-parse", "--is-inside-work-tree"], { cwd, stdio: "pipe" });
@@ -804,6 +860,23 @@ Flags:
804
860
  --no-incoming Don't write .maestro/incoming/ shadows for preserved files
805
861
  --verbose, -v Print classification for every file
806
862
  --help, -h Show this help
863
+
864
+ Per-file behaviour:
865
+ added — new upstream file → copy
866
+ updated — existed, byte-equal to your committed copy → safe overwrite
867
+ same — existed and already byte-identical to upstream → skip
868
+ ignored — matches a pattern in .maestroignore → never touched (any state)
869
+ preserved — has uncommitted local edits → keep yours, upstream lands at
870
+ .maestro/incoming/<path> for manual diff
871
+ mergeKept — under agents/ → never overwrite (custom agents preserved)
872
+ forced — overwritten by --force-overwrite; backup at .maestro/backup/<path>
873
+
874
+ .maestroignore format (gitignore-style, top-down, last match wins):
875
+ scripts/slack-send.sh exact file
876
+ scripts/daemon/ directory and everything beneath
877
+ scripts/send-email-*.py single-segment glob
878
+ workflows/** recursive glob
879
+ !workflows/quarterly/ un-ignore (force overwrite)
807
880
  `);
808
881
  return;
809
882
  }
@@ -823,12 +896,17 @@ Flags:
823
896
  }
824
897
 
825
898
  const dirty = inGit ? dirtyPathSet(cwd) : null;
899
+ const ignorePatterns = loadMaestroignore(cwd);
900
+ if (ignorePatterns) {
901
+ log(`.maestroignore loaded (${ignorePatterns.length} pattern${ignorePatterns.length === 1 ? "" : "s"})`);
902
+ }
826
903
 
827
904
  const banner = flags.dryRun ? "DRY RUN — " : "";
828
905
  log(`${banner}Upgrading framework files from @adaptic/maestro...`);
829
906
 
830
- const counts = { added: 0, updated: 0, same: 0, preserved: 0, mergeKept: 0, forced: 0 };
907
+ const counts = { added: 0, updated: 0, same: 0, ignored: 0, preserved: 0, mergeKept: 0, forced: 0 };
831
908
  const preservedFiles = [];
909
+ const ignoredFiles = [];
832
910
 
833
911
  for (const { path: relRoot, mode } of UPGRADE_PATHS) {
834
912
  const srcRoot = join(MAESTRO_ROOT, relRoot);
@@ -841,6 +919,18 @@ Flags:
841
919
  const dstFile = join(dstRoot, relFromRoot);
842
920
  const repoRel = relative(cwd, dstFile).split(sep).join("/");
843
921
 
922
+ const isIgnored = matchesIgnore(repoRel, ignorePatterns);
923
+
924
+ // Case 0: .maestroignore match — never touch, regardless of state.
925
+ // Checked BEFORE existsSync so that protected new-files-from-upstream
926
+ // are never silently introduced into the agent repo either.
927
+ if (isIgnored) {
928
+ counts.ignored++;
929
+ ignoredFiles.push(repoRel);
930
+ if (flags.verbose) console.log(` · ${repoRel} (ignored via .maestroignore)`);
931
+ continue;
932
+ }
933
+
844
934
  // Case 1: new file (doesn't exist in agent) — always copy.
845
935
  if (!existsSync(dstFile)) {
846
936
  if (!flags.dryRun) {
@@ -929,6 +1019,7 @@ Flags:
929
1019
  if (counts.added) ok(`${counts.added} added (new files)`);
930
1020
  if (counts.updated) ok(`${counts.updated} updated (vendored, no local edits)`);
931
1021
  if (counts.same) console.log(` = ${counts.same} already up-to-date`);
1022
+ if (counts.ignored) console.log(` · ${counts.ignored} ignored (.maestroignore protected)`);
932
1023
  if (counts.mergeKept) console.log(` ~ ${counts.mergeKept} merge-mode kept (agents/ custom files preserved)`);
933
1024
  if (counts.preserved) warn(`${counts.preserved} preserved (local edits — kept your version)`);
934
1025
  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.1",
4
4
  "description": "Maestro — Autonomous AI agent operating system. Deploy AI employees on dedicated Mac minis.",
5
5
  "type": "module",
6
6
  "bin": {