@yemi33/minions 0.1.2351 → 0.1.2352

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/engine/queries.js +117 -59
  2. package/package.json +1 -1
package/engine/queries.js CHANGED
@@ -2878,63 +2878,72 @@ function _resolveCommonGitDir(gitDir) {
2878
2878
  return path.isAbsolute(raw) ? raw : path.resolve(gitDir, raw);
2879
2879
  }
2880
2880
 
2881
- // Enumerate the per-project git ref files we watch for cache-busting:
2882
- // logs/HEAD (per-worktree gitdir), FETCH_HEAD + refs/remotes/origin/* (common
2883
- // gitdir for linked worktrees). Same paths as the fast-state mtime tracker
2884
- // so callers see a coherent view across surfaces.
2881
+ // Enumerate the per-project git ref files we watch for cache-busting, split
2882
+ // into two groups (W-mrb6qnuc000785b2):
2883
+ // identityFiles HEAD + logs/HEAD, resolved against the PER-WORKTREE
2884
+ // gitdir (`_resolveGitDir`). These are never shared across linked
2885
+ // worktrees of the same repo, so a change here means THIS worktree's
2886
+ // own branch/commit identity moved (opg-microsoft/minions#295, #381) —
2887
+ // the only signal allowed to gate `gitStale: true` / hide the branch
2888
+ // name in the dashboard.
2889
+ // sharedFiles — FETCH_HEAD + refs/remotes/origin/<comparator>, resolved
2890
+ // against the COMMON gitdir (`_resolveCommonGitDir`). These are written
2891
+ // by ANY linked worktree of the same repo doing a `git fetch` — a
2892
+ // legitimate signal that ahead/behind against origin may be stale and
2893
+ // worth recomputing, but NOT a signal that this worktree's own branch
2894
+ // identity changed. Lumping these into the same boolean as identityFiles
2895
+ // produced a permanent "(refreshing...)" chip on repos with several
2896
+ // active worktrees sharing one commondir, since some other worktree's
2897
+ // fetch touches these files constantly.
2898
+ // Same paths as the fast-state mtime tracker so callers see a coherent view
2899
+ // across surfaces.
2885
2900
  function _projectGitRefFiles(localPath, configuredMainBranch) {
2886
2901
  const gitDir = _resolveGitDir(localPath);
2887
2902
  if (!gitDir) return null;
2888
2903
  const commonGitDir = _resolveCommonGitDir(gitDir);
2889
- const files = [
2904
+ const identityFiles = [
2890
2905
  // opg-microsoft/minions#381 — track HEAD directly so a bare `git checkout`
2891
2906
  // (which rewrites .git/HEAD but may not advance logs/HEAD when reflog is
2892
2907
  // disabled or when the snapshot already reflects the post-checkout mtime)
2893
- // still triggers refsAdvanced=true and the gitStale marker on the next call.
2908
+ // still triggers identityAdvanced=true and the gitStale marker on the
2909
+ // next call.
2894
2910
  path.join(gitDir, 'HEAD'),
2895
2911
  path.join(gitDir, 'logs', 'HEAD'),
2912
+ ];
2913
+ const sharedFiles = [
2896
2914
  path.join(commonGitDir, 'FETCH_HEAD'),
2897
2915
  ];
2898
2916
  const comparator = configuredMainBranch && String(configuredMainBranch).trim();
2899
2917
  if (comparator) {
2900
- files.push(path.join(commonGitDir, 'refs', 'remotes', 'origin', comparator));
2918
+ sharedFiles.push(path.join(commonGitDir, 'refs', 'remotes', 'origin', comparator));
2901
2919
  }
2902
- return files;
2920
+ return { identityFiles, sharedFiles };
2903
2921
  }
2904
2922
 
2905
- // Snapshot mtimeMs for each ref file. Missing files record `null`. Used as
2906
- // the baseline that the next `getProjectGitStatus` call compares against
2907
- // inequality, not threshold-vs-timestamp, so the result is precision-
2908
- // independent (Windows `Date.now()` can be 15ms coarse while NTFS mtime is
2923
+ // Snapshot mtimeMs for each ref file, grouped by identity vs shared (see
2924
+ // `_projectGitRefFiles`). Missing files record `null`. Used as the baseline
2925
+ // that the next `getProjectGitStatus` call compares against — inequality,
2926
+ // not threshold-vs-timestamp, so the result is precision-independent
2927
+ // (Windows `Date.now()` can be 15ms coarse while NTFS mtime is
2909
2928
  // sub-millisecond, which used to make threshold checks fire spuriously on
2910
2929
  // freshly-written files).
2911
2930
  function _snapshotProjectGitRefMtimes(localPath, configuredMainBranch) {
2912
- const files = _projectGitRefFiles(localPath, configuredMainBranch);
2913
- if (!files) return null;
2914
- const out = Object.create(null);
2915
- for (const f of files) {
2916
- try { out[f] = fs.statSync(f).mtimeMs; }
2917
- catch { out[f] = null; /* ENOENT recorded as null — flipping to present must bust */ }
2918
- }
2919
- return out;
2931
+ const groups = _projectGitRefFiles(localPath, configuredMainBranch);
2932
+ if (!groups) return null;
2933
+ const statMtime = f => {
2934
+ try { return fs.statSync(f).mtimeMs; }
2935
+ catch { return null; /* ENOENT recorded as null — flipping to present must bust */ }
2936
+ };
2937
+ const identity = Object.create(null);
2938
+ for (const f of groups.identityFiles) identity[f] = statMtime(f);
2939
+ const shared = Object.create(null);
2940
+ for (const f of groups.sharedFiles) shared[f] = statMtime(f);
2941
+ return { identity, shared };
2920
2942
  }
2921
2943
 
2922
- // Return true when ANY tracked ref file's mtime (or existence) differs from
2923
- // the snapshot captured during the last probe. Replaces the older threshold-
2924
- // vs-cachedTs check that suffered from `Date.now()`/`mtimeMs` resolution
2925
- // races on Windows. Lets `getProjectGitStatus` bypass its 15s TTL after
2926
- // `git pull`, `git fetch`, `git checkout`, etc. so the next /api/status
2927
- // reflects the new HEAD / ahead-behind within one SPA poll instead of
2928
- // waiting out the TTL (W-mphdmr8c00030124). Cost: 2-3 statSync per call —
2929
- // well under the 'cheap' budget.
2930
- function _projectGitRefsAdvancedSince(localPath, configuredMainBranch, snapshot) {
2931
- // No snapshot yet (legacy entry shape OR first call) — preserve the
2932
- // current cached value so the TTL-only fast-path still works. A real
2933
- // change still surfaces on the next /api/status because the fast-state
2934
- // mtime tracker watches the same files and will bust the upstream cache.
2935
- if (!snapshot) return false;
2936
- const current = _snapshotProjectGitRefMtimes(localPath, configuredMainBranch);
2937
- if (!current) return false;
2944
+ // Compare two flat {file: mtimeMs} maps and return true if ANY tracked
2945
+ // file's mtime (or existence) differs between them.
2946
+ function _refGroupChanged(current, snapshot) {
2938
2947
  for (const file of Object.keys(snapshot)) {
2939
2948
  if (current[file] !== snapshot[file]) return true;
2940
2949
  }
@@ -2946,16 +2955,54 @@ function _projectGitRefsAdvancedSince(localPath, configuredMainBranch, snapshot)
2946
2955
  return false;
2947
2956
  }
2948
2957
 
2958
+ // Return `{ identityAdvanced, aheadBehindAdvanced }` describing which
2959
+ // tracked ref groups changed since `snapshot` (W-mrb6qnuc000785b2):
2960
+ // identityAdvanced — THIS worktree's own HEAD/logs/HEAD moved. The only
2961
+ // signal that should gate `gitStale: true`.
2962
+ // aheadBehindAdvanced — identityAdvanced OR the shared commondir files
2963
+ // (FETCH_HEAD / refs/remotes/origin/<comparator>)
2964
+ // changed — from a fetch in ANY linked worktree of
2965
+ // the same repo. Gates nulling ahead/behind and
2966
+ // bypassing the TTL to schedule a refresh, but never
2967
+ // gates gitStale on its own.
2968
+ // Replaces the older single-boolean threshold-vs-cachedTs check that
2969
+ // suffered from `Date.now()`/`mtimeMs` resolution races on Windows. Lets
2970
+ // `getProjectGitStatus` bypass its 15s TTL after `git pull`, `git fetch`,
2971
+ // `git checkout`, etc. so the next /api/status reflects the new HEAD /
2972
+ // ahead-behind within one SPA poll instead of waiting out the TTL
2973
+ // (W-mphdmr8c00030124). Cost: 2-4 statSync per call — well under the
2974
+ // 'cheap' budget.
2975
+ function _projectGitRefsAdvancedSince(localPath, configuredMainBranch, snapshot) {
2976
+ // No snapshot yet (legacy entry shape OR first call) — preserve the
2977
+ // current cached value so the TTL-only fast-path still works. A real
2978
+ // change still surfaces on the next /api/status because the fast-state
2979
+ // mtime tracker watches the same files and will bust the upstream cache.
2980
+ if (!snapshot) return { identityAdvanced: false, aheadBehindAdvanced: false };
2981
+ const current = _snapshotProjectGitRefMtimes(localPath, configuredMainBranch);
2982
+ if (!current) return { identityAdvanced: false, aheadBehindAdvanced: false };
2983
+ const identityAdvanced = _refGroupChanged(current.identity, snapshot.identity);
2984
+ const sharedAdvanced = _refGroupChanged(current.shared, snapshot.shared);
2985
+ return { identityAdvanced, aheadBehindAdvanced: identityAdvanced || sharedAdvanced };
2986
+ }
2987
+
2949
2988
  function getProjectGitStatus(localPath, configuredMainBranch = null) {
2950
2989
  const norm = String(localPath || '').replace(/\\/g, '/');
2951
2990
  if (!norm) return PROJECT_GIT_STATUS_MISSING;
2952
2991
  const key = _projectGitStatusCacheKey(localPath, configuredMainBranch);
2953
2992
  const now = Date.now();
2954
2993
  const cached = _projectGitStatusCache.get(key);
2955
- // Compute ref-mtime freshness once. Used twice below: to gate the TTL
2956
- // fast-path AND to decide whether the stale-while-revalidate return
2957
- // should null out the known-stale ahead/behind counters (#2848).
2958
- const refsAdvanced = !!(cached && _projectGitRefsAdvancedSince(localPath, configuredMainBranch, cached.refMtimes));
2994
+ // Compute ref-mtime freshness once. Used below to gate the TTL fast-path
2995
+ // AND to decide whether the stale-while-revalidate return should null out
2996
+ // the known-stale ahead/behind counters (#2848) and/or flag gitStale
2997
+ // (opg-microsoft/minions#295, #381) split so a fetch from an unrelated
2998
+ // worktree sharing this repo's commondir (FETCH_HEAD /
2999
+ // refs/remotes/origin/<comparator>) only refreshes ahead/behind quietly
3000
+ // and never flags THIS project's branch display as stale
3001
+ // (W-mrb6qnuc000785b2).
3002
+ const advancement = cached
3003
+ ? _projectGitRefsAdvancedSince(localPath, configuredMainBranch, cached.refMtimes)
3004
+ : { identityAdvanced: false, aheadBehindAdvanced: false };
3005
+ const refsAdvanced = advancement.aheadBehindAdvanced;
2959
3006
  // Within TTL: short-circuit ONLY when no tracked git ref has advanced
2960
3007
  // past cached.ts. Without the mtime gate, a freshly-pulled repo serves
2961
3008
  // the pre-pull ahead/behind counts for up to 15s + one SPA poll (~19s
@@ -2989,28 +3036,39 @@ function getProjectGitStatus(localPath, configuredMainBranch = null) {
2989
3036
  if (!cached) return PROJECT_GIT_STATUS_PENDING;
2990
3037
  // When tracked git refs have advanced past the cached snapshot, the
2991
3038
  // cached ahead/behind counts are known to be stale (e.g. user just ran
2992
- // `git pull` / `git fetch` and the local checkout is now current).
2993
- // Returning the cached value as-is would briefly display a misleading
2994
- // "behind: 8" after the user has already pulled to current — see
2995
- // yemi33/minions#2848. Null those specific counters so the dashboard
3039
+ // `git pull` / `git fetch` from THIS worktree or any other worktree
3040
+ // sharing the same repo commondir and the local checkout is now
3041
+ // current). Returning the cached value as-is would briefly display a
3042
+ // misleading "behind: 8" after the user has already pulled to current —
3043
+ // see yemi33/minions#2848. Null those specific counters so the dashboard
2996
3044
  // shows a pending state for ahead/behind until the background probe
2997
- // settles. TTL-expiry fall-throughs (refsAdvanced=false) still return the
2998
- // prior counters because nothing local has actually changed there.
3045
+ // settles. TTL-expiry fall-throughs (aheadBehindAdvanced=false) still
3046
+ // return the prior counters because nothing has actually changed there.
2999
3047
  //
3000
- // opg-microsoft/minions#295: gitBranch/gitDirty on the cached value are
3001
- // ALSO known-stale once refs have advanced (e.g. the operator ran
3002
- // `git switch` / `git pull` directly in a live checkout outside the
3003
- // dashboard). The cached value still carries the OLD branch name, so a
3004
- // consumer rendering `gitBranch` as authoritative briefly shows the
3005
- // pre-switch branch as if current. We keep the cached gitBranch/gitDirty
3006
- // on the object (so the next poll can self-correct without a flash of
3007
- // "missing"), but add a `gitStale: true` marker so render paths can avoid
3008
- // presenting the known-stale branch/dirty as current and fall back to a
3009
- // neutral "refreshing" presentation. The async refresh scheduled above
3010
- // self-corrects on the next poll. We do NOT add a synchronous git probe
3011
- // here status rebuild must never block the event loop.
3048
+ // opg-microsoft/minions#295 / #381: gitBranch/gitDirty on the cached
3049
+ // value are ALSO known-stale, but ONLY when THIS project's OWN
3050
+ // per-worktree HEAD/logs/HEAD moved (identityAdvanced) e.g. the
3051
+ // operator ran `git switch` / `git pull` directly in a live checkout
3052
+ // outside the dashboard, or a bare `git checkout` rewrote HEAD. A shared
3053
+ // commondir file (FETCH_HEAD / refs/remotes/origin/<comparator>) changing
3054
+ // because some OTHER linked worktree of the same repo fetched is NOT
3055
+ // evidence this project's branch identity changed gating gitStale on
3056
+ // that shared signal produced a permanently-stuck "(refreshing...)" chip
3057
+ // on repos with several active worktrees (W-mrb6qnuc000785b2). The cached
3058
+ // value still carries the OLD branch name, so a consumer rendering
3059
+ // `gitBranch` as authoritative briefly shows the pre-switch branch as if
3060
+ // current when identityAdvanced fires. We keep the cached
3061
+ // gitBranch/gitDirty on the object (so the next poll can self-correct
3062
+ // without a flash of "missing"), but add a `gitStale: true` marker so
3063
+ // render paths can avoid presenting the known-stale branch/dirty as
3064
+ // current and fall back to a neutral "refreshing" presentation. The async
3065
+ // refresh scheduled above self-corrects on the next poll. We do NOT add a
3066
+ // synchronous git probe here — status rebuild must never block the event
3067
+ // loop.
3012
3068
  if (refsAdvanced) {
3013
- return { ...cached.value, ahead: null, behind: null, gitStale: true };
3069
+ const next = { ...cached.value, ahead: null, behind: null };
3070
+ if (advancement.identityAdvanced) next.gitStale = true;
3071
+ return next;
3014
3072
  }
3015
3073
  return cached.value;
3016
3074
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yemi33/minions",
3
- "version": "0.1.2351",
3
+ "version": "0.1.2352",
4
4
  "description": "Multi-agent AI dev team that runs from ~/.minions/ — five autonomous agents share a single engine, dashboard, and knowledge base",
5
5
  "bin": {
6
6
  "minions": "bin/minions.js"