@yemi33/minions 0.1.2361 → 0.1.2363

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/dashboard.js CHANGED
@@ -1373,6 +1373,11 @@ function linkPullRequestForTracking({ url, title, project: projectName, contextO
1373
1373
  }, {
1374
1374
  project: targetProject,
1375
1375
  itemId: linkedWorkItemId,
1376
+ // W-mrdutzdr000t22f4 — a POST /api/pull-requests/link call is deliberate
1377
+ // user intent to (re)track this PR. Unlike passive re-enrollment paths
1378
+ // (pollers, enrollPrFromCanonicalId), this call site should clear a
1379
+ // tombstone left by a prior explicit delete rather than silently no-op.
1380
+ allowResurrect: true,
1376
1381
  });
1377
1382
  return { ...result, prPath, targetProject, projectResolution, prNum };
1378
1383
  }
@@ -13844,16 +13849,25 @@ What would you like to discuss or change? When you're happy, say "approve" and I
13844
13849
  } catch (e) {
13845
13850
  return jsonReply(res, e.statusCode || 400, { error: e.message }, req);
13846
13851
  }
13847
- const { id: prId, prPath, prNum, created, linked, targetProject, projectResolution } = linkResult;
13852
+ const { id: prId, prPath, prNum, created, linked, targetProject, projectResolution, skipped, resurrected } = linkResult;
13848
13853
  invalidateStatusCache();
13854
+ // W-mrdutzdr000t22f4 — upsertPullRequestRecord can leave the record
13855
+ // untouched (`skipped: true`) for reasons other than a resurrect, e.g.
13856
+ // a legitimate cross-scope tombstone match it still refuses to touch.
13857
+ // Surface that instead of always claiming success so a caller relying
13858
+ // on "PR linked." isn't misled into thinking the record changed.
13849
13859
  jsonReply(res, 200, {
13850
13860
  ok: true,
13851
13861
  id: prId,
13852
13862
  created,
13853
13863
  linked,
13864
+ skipped: skipped === true,
13865
+ resurrected: resurrected === true,
13854
13866
  project: targetProject?.name || 'central',
13855
13867
  projectResolution,
13856
- message: projectResolution?.message || 'PR linked.',
13868
+ message: skipped
13869
+ ? 'PR was not linked: an existing tracked record blocked the update.'
13870
+ : (projectResolution?.message || 'PR linked.'),
13857
13871
  });
13858
13872
 
13859
13873
  // Async-enrich: fetch title, description, branch, author from GitHub/ADO API
@@ -14099,8 +14113,14 @@ What would you like to discuss or change? When you're happy, say "approve" and I
14099
14113
  path.join(MINIONS_DIR, 'pull-requests.json'),
14100
14114
  ];
14101
14115
  let found = false;
14116
+ // Issue #803: tombstone EVERY scope that has a copy of this PR id, not
14117
+ // just the first one found. When enrollPrFromCanonicalId (issue #802)
14118
+ // has left a stray cross-scope duplicate (e.g. a legit per-project row
14119
+ // plus a stray central-scope copy), stopping at the first match leaves
14120
+ // the other scope's copy fully untouched — queries.js#getPullRequests
14121
+ // then de-dupes by picking that untouched duplicate as the "winner"
14122
+ // and silently resurrects the "deleted" PR on the next read.
14102
14123
  for (const prPath of prPaths) {
14103
- if (found) break;
14104
14124
  // Issue #384: use a tombstone (userDeleted:true) instead of splice so
14105
14125
  // the next poller cycle cannot re-add the PR. The entry stays in the
14106
14126
  // file but is filtered from API responses and skipped by pollers.
@@ -1378,7 +1378,20 @@ async function enrollPrFromCanonicalId(canonicalPrId, project, opts = {}) {
1378
1378
  // pr-links stays consistent (#779 pattern).
1379
1379
  if (opts.itemId && Array.isArray(existing.prdItems) && !existing.prdItems.includes(opts.itemId)) {
1380
1380
  try {
1381
- const targetPath = project ? shared.projectPrPath(project) : path.join(MINIONS_DIR, 'pull-requests.json');
1381
+ // W-mrduef3q000m2172 derive targetPath from the scope the store
1382
+ // actually returned `existing` under (existing._scope), NOT from the
1383
+ // current call's `project` argument. When the two differ (e.g. the
1384
+ // record lives under 'central' but this enrollment call was made
1385
+ // with a specific project), re-deriving from `project` sends the
1386
+ // upsert to the WRONG scope's file/array. upsertPullRequestRecord's
1387
+ // tombstone guard then finds no matching record in that (wrong)
1388
+ // scope and falls through to insert-as-new, silently clearing any
1389
+ // userDeleted tombstone on the real record. See issue writeup for
1390
+ // the confirmed repro.
1391
+ const store = require('./pull-requests-store');
1392
+ const targetPath = existing._scope
1393
+ ? store._filePathForScope(existing._scope)
1394
+ : (project ? shared.projectPrPath(project) : path.join(MINIONS_DIR, 'pull-requests.json'));
1382
1395
  shared.upsertPullRequestRecord(targetPath, {
1383
1396
  id: normalizedCanonicalId,
1384
1397
  prNumber,
package/engine/queries.js CHANGED
@@ -995,12 +995,25 @@ function getPullRequests(config) {
995
995
  // Then merge non-conflicting fields from the loser into the winner so we
996
996
  // never lose prdItems, sourcePlan, itemType, minionsReview, or
997
997
  // _automationFixCauses if only the loser carried them.
998
+ // Issue #803 — defense in depth: if a canonical PR id has a tombstoned
999
+ // (userDeleted:true) copy in ANY scope, treat the whole id as deleted, not
1000
+ // just that one record. Without this, a delete that (for whatever reason)
1001
+ // only tombstones one scope's copy would still have its untouched
1002
+ // cross-scope duplicate survive grouping below and get picked as the
1003
+ // dedupe "winner", silently resurrecting a PR the user just deleted.
1004
+ const tombstonedIds = new Set();
1005
+ for (const pr of sqlPrs) {
1006
+ if (pr && pr.id && pr.userDeleted === true) tombstonedIds.add(pr.id);
1007
+ }
1008
+
998
1009
  const groups = [];
999
1010
  const groupsById = new Map();
1000
1011
  for (const pr of sqlPrs) {
1001
1012
  if (!pr || !pr.id) continue;
1002
- // Issue #384: skip tombstoned records (user explicitly deleted)
1013
+ // Issue #384/#803: skip tombstoned records (user explicitly deleted), and
1014
+ // skip ANY record sharing an id that has a tombstoned copy elsewhere.
1003
1015
  if (pr.userDeleted === true) continue;
1016
+ if (tombstonedIds.has(pr.id)) continue;
1004
1017
  if (!groupsById.has(pr.id)) {
1005
1018
  const g = [];
1006
1019
  groupsById.set(pr.id, g);
package/engine/shared.js CHANGED
@@ -7873,7 +7873,7 @@ function collapseDuplicatePrRecords(prPath, { project = null } = {}) {
7873
7873
  * manually records a PR for a work item must use this helper so the PR record
7874
7874
  * and the canonical work-item attachment are created together and idempotently.
7875
7875
  */
7876
- function upsertPullRequestRecord(prPath, entry, { project = null, itemId = null, itemIds = null, beforeInsert = null } = {}) {
7876
+ function upsertPullRequestRecord(prPath, entry, { project = null, itemId = null, itemIds = null, beforeInsert = null, allowResurrect = false } = {}) {
7877
7877
  if (!prPath) throw new Error('prPath required');
7878
7878
  if (!entry || typeof entry !== 'object') throw new Error('entry required');
7879
7879
 
@@ -7906,6 +7906,7 @@ function upsertPullRequestRecord(prPath, entry, { project = null, itemId = null,
7906
7906
  let created = false;
7907
7907
  let linked = false;
7908
7908
  let skipped = false;
7909
+ let resurrected = false;
7909
7910
  let record = null;
7910
7911
 
7911
7912
  mutatePullRequests(prPath, (prs) => {
@@ -7914,9 +7915,37 @@ function upsertPullRequestRecord(prPath, entry, { project = null, itemId = null,
7914
7915
  // Issue #384: tombstoned records (userDeleted:true) must not be re-activated
7915
7916
  // by any poller path. Return skipped so reconcilePrs / shared-branch-reconcile
7916
7917
  // cannot resurrect a PR the user explicitly removed.
7918
+ //
7919
+ // W-mrdutzdr000t22f4 — passive re-enrollment (pollers, enrollPrFromCanonicalId)
7920
+ // must keep respecting the tombstone, but a deliberate user-initiated relink
7921
+ // via POST /api/pull-requests/link is explicit intent to bring the PR back.
7922
+ // Callers that want that behavior opt in with `allowResurrect: true`, which
7923
+ // clears the tombstone here and lets the normal field-merge logic below run.
7917
7924
  if (target && target.userDeleted === true) {
7918
- skipped = true;
7919
- return prs;
7925
+ if (!allowResurrect) {
7926
+ skipped = true;
7927
+ return prs;
7928
+ }
7929
+ delete target.userDeleted;
7930
+ delete target.userDeletedAt;
7931
+ resurrected = true;
7932
+ }
7933
+ // W-mrduef3q000m2172 — defense-in-depth: `prs` here is scoped to a single
7934
+ // scope's array (mutatePullRequests routes by prPath). If a caller
7935
+ // resolved the wrong scope for an already-tombstoned canonical id (the
7936
+ // record actually lives in a DIFFERENT scope's file), the above check
7937
+ // never sees it and would otherwise fall through to insert-as-new,
7938
+ // silently clearing the tombstone. Check globally before inserting.
7939
+ if (!target) {
7940
+ try {
7941
+ const store = require('./pull-requests-store');
7942
+ const globalMatch = (store.readAllPullRequests() || [])
7943
+ .find(pr => pr && pr.id === canonicalId && pr.userDeleted === true);
7944
+ if (globalMatch) {
7945
+ skipped = true;
7946
+ return prs;
7947
+ }
7948
+ } catch { /* best-effort global tombstone check — fall through on failure */ }
7920
7949
  }
7921
7950
  // P-e9f0a2b4 — When findPrRecord returns null but multiple records share the
7922
7951
  // same prNumber (e.g. cross-scope contamination or a project-config change
@@ -7980,7 +8009,7 @@ function upsertPullRequestRecord(prPath, entry, { project = null, itemId = null,
7980
8009
  }
7981
8010
  }
7982
8011
 
7983
- return { id: canonicalId, prNumber, created, linked, skipped, record };
8012
+ return { id: canonicalId, prNumber, created, linked, skipped, resurrected, record };
7984
8013
  }
7985
8014
 
7986
8015
  // ─── PR Reference → URL Derivation ───────────────────────────────────────────
package/engine.js CHANGED
@@ -28,13 +28,14 @@ require('./engine/stdio-timestamps').installIfNotInstalled();
28
28
 
29
29
  const fs = require('fs');
30
30
  const path = require('path');
31
+ const os = require('os');
31
32
  const crypto = require('crypto');
32
33
  const shared = require('./engine/shared');
33
34
  const { exec, execAsync, execSilent, runFile, ts, ENGINE_DEFAULTS,
34
35
  WI_STATUS, DONE_STATUSES, WORK_TYPE, PLAN_STATUS, PRD_ITEM_STATUS, PRD_MATERIALIZABLE, PR_STATUS, REVIEW_STATUS, DISPATCH_RESULT, AGENT_STATUS,
35
36
  FAILURE_CLASS, resolvePollFlag } = shared;
36
37
  const { resolveRuntime } = require('./engine/runtimes');
37
- const { assertStaleHeadOk } = require('./engine/spawn-agent');
38
+ const { assertStaleHeadOk, preApproveWorkspaceMcps, computeAddDirs } = require('./engine/spawn-agent');
38
39
  const adoGitAuth = require('./engine/ado-git-auth');
39
40
  const queries = require('./engine/queries');
40
41
  const dispatchEvents = require('./engine/dispatch-events');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yemi33/minions",
3
- "version": "0.1.2361",
3
+ "version": "0.1.2363",
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"