@yemi33/minions 0.1.2393 → 0.1.2394

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.
@@ -2629,7 +2629,7 @@ function normalizePrFixBranchName(branch) {
2629
2629
  }
2630
2630
 
2631
2631
  function getPrFixBaselineHead(pr) {
2632
- return String(pr?.headSha || pr?._adoSourceCommit || pr?._adoHeadCommit || '').trim();
2632
+ return String(pr?.headRefOid || pr?.headSha || pr?._adoSourceCommit || pr?._adoHeadCommit || '').trim();
2633
2633
  }
2634
2634
 
2635
2635
  function findPrFixWorktree(meta, project, config) {
@@ -5962,7 +5962,14 @@ async function runPostCompletionHooks(dispatchItem, agentId, code, stdout, confi
5962
5962
  }
5963
5963
  if (effectiveSuccess && !skipDoneStatus) {
5964
5964
  // Coding WIs and direct PR-sourced fix dispatches share this follow-up path.
5965
- try { autoDispatchLiveValidationWi(meta, config, dispatchItem); } catch (err) { log('warn', `autoDispatchLiveValidationWi: ${err.message}`); }
5965
+ const verifiedHeadSha = prFixBranchChange?.evidence === 'remote-head'
5966
+ ? prFixBranchChange.afterHead
5967
+ : '';
5968
+ try {
5969
+ autoDispatchLiveValidationWi(meta, config, dispatchItem, { verifiedHeadSha });
5970
+ } catch (err) {
5971
+ log('warn', `autoDispatchLiveValidationWi: ${err.message}`);
5972
+ }
5966
5973
  }
5967
5974
  // Failure retry is handled by completeDispatch in dispatch.js — not duplicated here.
5968
5975
  // Only clear _decomposing flag on failure so decompose items don't get permanently stuck.
@@ -6943,9 +6950,9 @@ function collapseAllDuplicatePrRecords(config) {
6943
6950
  // always a canonical `test` WI with the PR-focused `build-and-test` playbook;
6944
6951
  // liveValidation.type controls routing, not the semantic type of the task.
6945
6952
  //
6946
- // Deduplicates per codingWiId: any non-terminal WI with
6947
- // meta.liveValidationFor === codingWiId blocks a second dispatch, so only one
6948
- // validation task is ever in flight per coding completion.
6953
+ // Deduplicates both per codingWiId and per canonical PR source revision. This
6954
+ // keeps concurrent/repeated fixes for one unchanged PR head from queueing
6955
+ // duplicate validations while allowing a fresh validation after the head moves.
6949
6956
  //
6950
6957
  // W-mr9u39az000db5c2 — guard against a self-referential recursive cascade.
6951
6958
  // When liveValidation.type overlaps with a coding WI's own type set (e.g.
@@ -6970,7 +6977,7 @@ const ELIGIBLE_LIVE_VALIDATION_TYPES = new Set([
6970
6977
  WORK_TYPE.BUILD_FIX_COMPLEX,
6971
6978
  ]);
6972
6979
 
6973
- function autoDispatchLiveValidationWi(meta, config, dispatchItem = null) {
6980
+ function autoDispatchLiveValidationWi(meta, config, dispatchItem = null, options = {}) {
6974
6981
  const item = meta?.item || null;
6975
6982
  const sourceType = item?.type || dispatchItem?.type || null;
6976
6983
  const sourceKey = item?.id || dispatchItem?.id || null;
@@ -7033,27 +7040,48 @@ function autoDispatchLiveValidationWi(meta, config, dispatchItem = null) {
7033
7040
  const prTarget = prUrl || (prId != null ? String(prId) : null);
7034
7041
  if (!prTarget) return;
7035
7042
 
7043
+ const prIdentityRef = {
7044
+ id: prId != null ? String(prId) : prTarget,
7045
+ url: prUrl || '',
7046
+ };
7047
+ let currentPr = typeof prRef === 'object' ? prRef : null;
7048
+ try {
7049
+ currentPr = shared.findPrRecord(shared.readPullRequests(project), prIdentityRef, project) || currentPr;
7050
+ } catch (err) {
7051
+ log('warn', `liveValidation: could not resolve tracked PR ${prTarget}: ${err.message}`);
7052
+ }
7053
+ const liveValidationPrId = shared.getPrIdentityKey(currentPr || prIdentityRef, project);
7054
+ if (!liveValidationPrId) return;
7055
+ const liveValidationHeadSha = String(options?.verifiedHeadSha || '').trim()
7056
+ || getPrFixBaselineHead(currentPr);
7057
+
7036
7058
  const codingWiId = sourceKey;
7037
7059
  const wiScope = item ? resolveWorkItemScope(meta) : projectName;
7038
7060
  if (!wiScope) return;
7039
7061
 
7040
7062
  try {
7041
- mutateWorkItems(wiScope, items => {
7063
+ mutateWorkItems(wiScope, (items, comparisonItems) => {
7042
7064
  if (!Array.isArray(items)) return items;
7043
7065
 
7044
- // Dedup: only one validation WI in flight per coding WI. Any non-terminal
7045
- // WI already tracking this coding WI (meta.liveValidationFor) blocks a
7046
- // second dispatch, regardless of its validation type. (Back-compat: WIs
7047
- // created before meta.liveValidationType existed are matched too, since
7048
- // we key solely on liveValidationFor.)
7049
- const existing = items.find(i =>
7050
- i &&
7051
- i.meta &&
7052
- i.meta.liveValidationFor === codingWiId &&
7053
- !PLAN_TERMINAL_STATUSES.has(i.status)
7054
- );
7066
+ // Keep the source-WI guard and revision guard in this same mutation as the
7067
+ // insert, and compare both supported scopes while the SQLite write
7068
+ // transaction is held. Legacy live-validation WIs predate the persisted
7069
+ // revision fields, so derive their identity from structured PR fields and
7070
+ // conservatively treat a missing head as matching until terminal.
7071
+ const existing = comparisonItems.find(i => {
7072
+ if (!i?.meta?.liveValidationFor || PLAN_TERMINAL_STATUSES.has(i.status)) return false;
7073
+ if (i.meta.liveValidationFor === codingWiId) return true;
7074
+
7075
+ const existingPrRef = i.meta.liveValidationPrId || shared.extractStructuredWorkItemPrRef(i);
7076
+ if (!existingPrRef) return false;
7077
+ const existingPrId = shared.getPrIdentityKey(existingPrRef, project);
7078
+ if (existingPrId !== liveValidationPrId) return false;
7079
+
7080
+ const existingHeadSha = String(i.meta.liveValidationHeadSha || '').trim();
7081
+ return !existingHeadSha || !liveValidationHeadSha || existingHeadSha === liveValidationHeadSha;
7082
+ });
7055
7083
  if (existing) {
7056
- log('info', `liveValidation: dedup — non-terminal validation WI ${existing.id} already exists for ${codingWiId}`);
7084
+ log('info', `liveValidation: dedup — non-terminal validation WI ${existing.id} blocks another validation for ${codingWiId} (${liveValidationPrId}@${liveValidationHeadSha || 'unknown'})`);
7057
7085
  return items;
7058
7086
  }
7059
7087
 
@@ -7081,6 +7109,8 @@ function autoDispatchLiveValidationWi(meta, config, dispatchItem = null) {
7081
7109
  meta: {
7082
7110
  liveValidationFor: codingWiId,
7083
7111
  liveValidationType: validationType,
7112
+ liveValidationPrId,
7113
+ ...(liveValidationHeadSha ? { liveValidationHeadSha } : {}),
7084
7114
  playbook: 'build-and-test',
7085
7115
  ...(dispatchItem?.id ? { sourceDispatchId: dispatchItem.id } : {}),
7086
7116
  },
@@ -7093,7 +7123,7 @@ function autoDispatchLiveValidationWi(meta, config, dispatchItem = null) {
7093
7123
  items.push(validationWi);
7094
7124
  log('info', `liveValidation: auto-dispatched validation WI ${validationWi.id} (type=${validationType}) for coding WI ${codingWiId}`);
7095
7125
  return items;
7096
- });
7126
+ }, { readScopes: ['central', projectName] });
7097
7127
  } catch (err) {
7098
7128
  log('warn', `autoDispatchLiveValidationWi for ${codingWiId}: ${err.message}`);
7099
7129
  }
package/engine/shared.js CHANGED
@@ -7702,12 +7702,17 @@ function readWorkItems(source = null) {
7702
7702
  return require('./work-items-store').readWorkItemsForScope(stateScope(source));
7703
7703
  }
7704
7704
 
7705
- function mutateWorkItems(source, mutator) {
7705
+ function mutateWorkItems(source, mutator, opts = {}) {
7706
7706
  const store = require('./work-items-store');
7707
- const { wrote, result } = store.applyWorkItemsMutation(stateScope(source), (items) => {
7707
+ const scope = stateScope(source);
7708
+ const normalizedOpts = { ...opts };
7709
+ if (Array.isArray(opts.readScopes)) {
7710
+ normalizedOpts.readScopes = opts.readScopes.map(stateScope);
7711
+ }
7712
+ const { wrote, result } = store.applyWorkItemsMutation(scope, (items, comparisonItems) => {
7708
7713
  if (!Array.isArray(items)) items = [];
7709
- return mutator(items) || items;
7710
- });
7714
+ return mutator(items, comparisonItems) || items;
7715
+ }, normalizedOpts);
7711
7716
  if (wrote) {
7712
7717
  try { require('./db-events').emitStateEvent('work_items'); } catch { /* optional */ }
7713
7718
  try { require('./queries').invalidateWorkItemsCache(); } catch { /* queries not loaded */ }
@@ -128,13 +128,14 @@ function _applyWorkItemsDiff(db, diff) {
128
128
 
129
129
  // Apply a mutation to the work-items array for a given scope. Wrapped in
130
130
  // a single transaction so the diff and apply can't race against a
131
- // concurrent reader/writer.
131
+ // concurrent reader/writer. Optional readScopes are exposed to the mutator
132
+ // as a combined comparison set; only target-scope mutations are persisted.
132
133
  //
133
134
  // Returns { wrote, result }:
134
135
  // wrote — true iff at least one INSERT/UPDATE/DELETE landed
135
136
  // result — the post-mutation array (legacy return shape of
136
137
  // mutateJsonFileLocked)
137
- function applyWorkItemsMutation(scope, mutator, opts) {
138
+ function applyWorkItemsMutation(scope, mutator, opts = {}) {
138
139
  const { getDb, withTransaction } = require('./db');
139
140
  const db = getDb();
140
141
 
@@ -144,8 +145,14 @@ function applyWorkItemsMutation(scope, mutator, opts) {
144
145
  // added by readAllWorkItems only and would round-trip as garbage into
145
146
  // the data column otherwise.
146
147
  for (const wi of before) { if (wi && wi._source) delete wi._source; }
148
+ const comparisonItems = [...before];
149
+ const readScopes = Array.isArray(opts.readScopes) ? opts.readScopes : [];
150
+ for (const readScope of new Set(readScopes)) {
151
+ if (readScope === scope) continue;
152
+ comparisonItems.push(...readWorkItemsForScope(readScope));
153
+ }
147
154
  const beforeSnapshot = JSON.parse(JSON.stringify(before));
148
- const next = mutator(before);
155
+ const next = mutator(before, comparisonItems);
149
156
  const after = (next === undefined || next === null)
150
157
  ? before
151
158
  : (Array.isArray(next) ? next : before);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yemi33/minions",
3
- "version": "0.1.2393",
3
+ "version": "0.1.2394",
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"